diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 44c38afdec6..29d5a95ea01 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -62,7 +62,7 @@ "json.schemas": [ { "fileMatch": ["homeassistant/components/*/manifest.json"], - "url": "./script/json_schemas/manifest_schema.json" + "url": "${containerWorkspaceFolder}/script/json_schemas/manifest_schema.json" } ] } diff --git a/.gitattributes b/.gitattributes index eca98fc228f..6a18819be9d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,14 @@ *.pcm binary Dockerfile.dev linguist-language=Dockerfile + +# Generated files +CODEOWNERS linguist-generated=true +Dockerfile linguist-generated=true +homeassistant/generated/*.py linguist-generated=true +mypy.ini linguist-generated=true +requirements.txt linguist-generated=true +requirements_all.txt linguist-generated=true +requirements_test_all.txt linguist-generated=true +requirements_test_pre_commit.txt linguist-generated=true +script/hassfest/docker/Dockerfile linguist-generated=true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 23365feffb7..792dacd8032 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -46,6 +46,8 @@ - This PR fixes or closes issue: fixes # - This PR is related to issue: - Link to documentation pull request: +- Link to developer documentation pull request: +- Link to frontend pull request: ## Checklist removed - "path": "/api/v1", - "product_name": "P1 meter", - "product_type": "HWE-P1", - "serial": "5c2fafabcdef", - }, - ), - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "invalid_discovery_parameters" - - -async def test_discovery_invalid_api(hass: HomeAssistant) -> None: - """Test discovery detecting invalid_api.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], port=80, @@ -251,16 +231,16 @@ async def test_discovery_invalid_api(hass: HomeAssistant) -> None: name="", properties={ "api_enabled": "1", - "path": "/api/not_v1", + "path": "/api/v1", "product_name": "P1 meter", - "product_type": "HWE-P1", + # "product_type": "HWE-P1", --> removed "serial": "5c2fafabcdef", }, ), ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "unsupported_api_version" + assert result["reason"] == "invalid_discovery_parameters" async def test_dhcp_discovery_updates_entry( @@ -274,7 +254,7 @@ async def test_dhcp_discovery_updates_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.0.0.127", hostname="HW-p1meter-aabbcc", macaddress="5c2fafabcdef", @@ -304,7 +284,7 @@ async def test_dhcp_discovery_updates_entry_fails( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.0.0.127", hostname="HW-p1meter-aabbcc", macaddress="5c2fafabcdef", @@ -326,7 +306,7 @@ async def test_dhcp_discovery_ignores_unknown( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="127.0.0.1", hostname="HW-p1meter-aabbcc", macaddress="5c2fafabcdef", @@ -337,6 +317,32 @@ async def test_dhcp_discovery_ignores_unknown( assert result.get("reason") == "unknown" +async def test_dhcp_discovery_aborts_for_v2_api( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery aborts when v2 API is detected. + + DHCP discovery requires authorization which is not yet implemented + """ + mock_homewizardenergy.device.side_effect = UnauthorizedError + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="1.0.0.127", + hostname="HW-p1meter-aabbcc", + macaddress="5c2fafabcdef", + ), + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "unsupported_api_version" + + async def test_discovery_flow_updates_new_ip( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -350,7 +356,7 @@ async def test_discovery_flow_updates_new_ip( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.0.0.127"), ip_addresses=[ip_address("1.0.0.127")], port=80, @@ -454,12 +460,12 @@ async def test_reauth_flow( result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["step_id"] == "reauth_enable_api" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + assert result["reason"] == "reauth_enable_api_successful" async def test_reauth_error( @@ -474,7 +480,7 @@ async def test_reauth_error( result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["step_id"] == "reauth_enable_api" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -608,3 +614,222 @@ async def test_reconfigure_cannot_connect( # changed entry assert mock_config_entry.data[CONF_IP_ADDRESS] == "1.0.0.127" + + +### TESTS FOR V2 IMPLEMENTATION ### + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_manual_flow_works_with_v2_api_support( + hass: HomeAssistant, + mock_homewizardenergy_v2: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test config flow accepts user configuration and triggers authorization when detected v2 support.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # Simulate v2 support but not authorized + mock_homewizardenergy_v2.device.side_effect = UnauthorizedError + mock_homewizardenergy_v2.get_token.side_effect = DisabledError + + with patch( + "homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "authorize" + + # Simulate user authorizing + mock_homewizardenergy_v2.device.side_effect = None + mock_homewizardenergy_v2.get_token.side_effect = None + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_manual_flow_detects_failed_user_authorization( + hass: HomeAssistant, + mock_homewizardenergy_v2: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test config flow accepts user configuration and detects failed button press by user.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # Simulate v2 support but not authorized + mock_homewizardenergy_v2.device.side_effect = UnauthorizedError + mock_homewizardenergy_v2.get_token.side_effect = DisabledError + + with patch( + "homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "authorize" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "authorize" + assert result["errors"] == {"base": "authorization_failed"} + + # Restore normal functionality + mock_homewizardenergy_v2.device.side_effect = None + mock_homewizardenergy_v2.get_token.side_effect = None + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reauth_flow_updates_token( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry_v2: MockConfigEntry, + mock_homewizardenergy_v2: MagicMock, +) -> None: + """Test reauth flow token is updated.""" + + mock_config_entry_v2.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry_v2.entry_id) + await hass.async_block_till_done() + + result = await mock_config_entry_v2.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm_update_token" + + # Simulate user pressing the button and getting a new token + mock_homewizardenergy_v2.get_token.return_value = "cool_new_token" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + # Verify that the token was updated + await hass.async_block_till_done() + assert ( + hass.config_entries.async_entries(DOMAIN)[0].data.get(CONF_TOKEN) + == "cool_new_token" + ) + assert len(mock_setup_entry.mock_calls) == 2 + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reauth_flow_handles_user_not_pressing_button( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry_v2: MockConfigEntry, + mock_homewizardenergy_v2: MagicMock, +) -> None: + """Test reauth flow token is updated.""" + + mock_config_entry_v2.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry_v2.entry_id) + await hass.async_block_till_done() + + result = await mock_config_entry_v2.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm_update_token" + assert result["errors"] is None + + # Simulate button not being pressed + mock_homewizardenergy_v2.get_token.side_effect = DisabledError + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "authorization_failed"} + + # Simulate user pressing the button and getting a new token + mock_homewizardenergy_v2.get_token.side_effect = None + mock_homewizardenergy_v2.get_token.return_value = "cool_new_token" + + # Successful reauth + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + # Verify that the token was updated + await hass.async_block_till_done() + assert ( + hass.config_entries.async_entries(DOMAIN)[0].data.get(CONF_TOKEN) + == "cool_new_token" + ) + assert len(mock_setup_entry.mock_calls) == 2 + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_discovery_with_v2_api_ask_authorization( + hass: HomeAssistant, + # mock_setup_entry: AsyncMock, + mock_homewizardenergy_v2: MagicMock, +) -> None: + """Test discovery detecting missing discovery info.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + port=443, + hostname="p1meter-abcdef.local.", + type="", + name="", + properties={ + "api_version": "2.0.0", + "id": "appliance/p1dongle/5c2fafabcdef", + "product_name": "P1 meter", + "product_type": "HWE-P1", + "serial": "5c2fafabcdef", + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + mock_homewizardenergy_v2.device.side_effect = UnauthorizedError + mock_homewizardenergy_v2.get_token.side_effect = DisabledError + + with patch( + "homeassistant.components.homewizard.config_flow.has_v2_api", return_value=True + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "authorize" + + mock_homewizardenergy_v2.get_token.side_effect = None + mock_homewizardenergy_v2.get_token.return_value = "cool_token" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_TOKEN] == "cool_token" diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index e3d7f4e6da9..c7063d497c3 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -21,6 +21,7 @@ from tests.typing import ClientSessionGenerator "SDM630", "HWE-KWH1", "HWE-KWH3", + "HWE-BAT", ], ) async def test_diagnostics( diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index a01f075ee61..9139ef80d12 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -2,30 +2,85 @@ from datetime import timedelta from unittest.mock import MagicMock +import weakref from freezegun.api import FrozenDateTimeFactory -from homewizard_energy.errors import DisabledError +from homewizard_energy.errors import DisabledError, UnauthorizedError import pytest from homeassistant.components.homewizard.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, async_fire_time_changed -async def test_load_unload( +async def test_load_unload_v1( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homewizardenergy: MagicMock, ) -> None: - """Test loading and unloading of integration.""" + """Test loading and unloading of integration with v1 config.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + weak_ref = weakref.ref(mock_config_entry.runtime_data) + assert weak_ref() is not None + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert len(mock_homewizardenergy.combined.mock_calls) == 1 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert weak_ref() is None + + +async def test_load_unload_v2( + hass: HomeAssistant, + mock_config_entry_v2: MockConfigEntry, + mock_homewizardenergy_v2: MagicMock, +) -> None: + """Test loading and unloading of integration with v2 config.""" + mock_config_entry_v2.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_v2.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry_v2.state is ConfigEntryState.LOADED + assert len(mock_homewizardenergy_v2.combined.mock_calls) == 1 + + await hass.config_entries.async_unload(mock_config_entry_v2.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry_v2.state is ConfigEntryState.NOT_LOADED + + +async def test_load_unload_v2_as_v1( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, +) -> None: + """Test loading and unloading of integration with v2 config, but without using it.""" + + # Simulate v2 config but as a P1 Meter + mock_config_entry = MockConfigEntry( + title="Device", + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "127.0.0.1", + CONF_TOKEN: "00112233445566778899ABCDEFABCDEF", + }, + unique_id="HWE-P1_5c2fafabcdef", + ) + mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED - assert len(mock_homewizardenergy.device.mock_calls) == 1 + assert len(mock_homewizardenergy.combined.mock_calls) == 1 await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -39,7 +94,7 @@ async def test_load_failed_host_unavailable( mock_homewizardenergy: MagicMock, ) -> None: """Test setup handles unreachable host.""" - mock_homewizardenergy.device.side_effect = TimeoutError() + mock_homewizardenergy.combined.side_effect = TimeoutError() mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -53,7 +108,7 @@ async def test_load_detect_api_disabled( mock_homewizardenergy: MagicMock, ) -> None: """Test setup detects disabled API.""" - mock_homewizardenergy.device.side_effect = DisabledError() + mock_homewizardenergy.combined.side_effect = DisabledError() mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -64,7 +119,7 @@ async def test_load_detect_api_disabled( assert len(flows) == 1 flow = flows[0] - assert flow.get("step_id") == "reauth_confirm" + assert flow.get("step_id") == "reauth_enable_api" assert flow.get("handler") == DOMAIN assert "context" in flow @@ -72,6 +127,31 @@ async def test_load_detect_api_disabled( assert flow["context"].get("entry_id") == mock_config_entry.entry_id +async def test_load_detect_invalid_token( + hass: HomeAssistant, + mock_config_entry_v2: MockConfigEntry, + mock_homewizardenergy_v2: MagicMock, +) -> None: + """Test setup detects invalid token.""" + mock_homewizardenergy_v2.combined.side_effect = UnauthorizedError() + mock_config_entry_v2.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry_v2.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry_v2.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm_update_token" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry_v2.entry_id + + @pytest.mark.usefixtures("mock_homewizardenergy") async def test_load_removes_reauth_flow( hass: HomeAssistant, @@ -115,7 +195,7 @@ async def test_disablederror_reloads_integration( assert len(flows) == 0 # Simulate DisabledError and wait for next update - mock_homewizardenergy.device.side_effect = DisabledError() + mock_homewizardenergy.combined.side_effect = DisabledError() freezer.tick(timedelta(seconds=5)) async_fire_time_changed(hass) @@ -128,5 +208,5 @@ async def test_disablederror_reloads_integration( assert len(flows) == 1 flow = flows[0] - assert flow.get("step_id") == "reauth_confirm" + assert flow.get("step_id") == "reauth_enable_api" assert flow.get("handler") == DOMAIN diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py index 623ba018dee..67e51cbafe2 100644 --- a/tests/components/homewizard/test_number.py +++ b/tests/components/homewizard/test_number.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from homewizard_energy.errors import DisabledError, RequestError +from homewizard_energy.models import CombinedModels, Measurement, State, System import pytest from syrupy.assertion import SnapshotAssertion @@ -13,7 +14,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -44,7 +45,9 @@ async def test_number_entities( # Test unknown handling assert state.state == "100" - mock_homewizardenergy.state.return_value.brightness = None + mock_homewizardenergy.combined.return_value = CombinedModels( + device=None, measurement=Measurement(), system=System(), state=State() + ) async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) await hass.async_block_till_done() @@ -53,7 +56,7 @@ async def test_number_entities( assert state.state == STATE_UNKNOWN # Test service methods - assert len(mock_homewizardenergy.state_set.mock_calls) == 0 + assert len(mock_homewizardenergy.state.mock_calls) == 0 await hass.services.async_call( number.DOMAIN, SERVICE_SET_VALUE, @@ -64,10 +67,10 @@ async def test_number_entities( blocking=True, ) - assert len(mock_homewizardenergy.state_set.mock_calls) == 1 - mock_homewizardenergy.state_set.assert_called_with(brightness=129) + assert len(mock_homewizardenergy.system.mock_calls) == 1 + mock_homewizardenergy.system.assert_called_with(status_led_brightness_pct=50) - mock_homewizardenergy.state_set.side_effect = RequestError + mock_homewizardenergy.system.side_effect = RequestError with pytest.raises( HomeAssistantError, match=r"^An error occurred while communicating with HomeWizard device$", @@ -82,7 +85,7 @@ async def test_number_entities( blocking=True, ) - mock_homewizardenergy.state_set.side_effect = DisabledError + mock_homewizardenergy.system.side_effect = DisabledError with pytest.raises( HomeAssistantError, match=r"^The local API is disabled$", diff --git a/tests/components/homewizard/test_repair.py b/tests/components/homewizard/test_repair.py new file mode 100644 index 00000000000..763af48b1a2 --- /dev/null +++ b/tests/components/homewizard/test_repair.py @@ -0,0 +1,86 @@ +"""Test the homewizard config flow.""" + +from unittest.mock import MagicMock, patch + +from homewizard_energy.errors import DisabledError + +from homeassistant.components.homewizard.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.repairs import ( + async_process_repairs_platforms, + process_repair_fix_flow, + start_repair_fix_flow, +) +from tests.typing import ClientSessionGenerator + + +async def test_repair_acquires_token( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homewizardenergy: MagicMock, + mock_homewizardenergy_v2: MagicMock, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair flow is able to obtain and use token.""" + + assert await async_setup_component(hass, "repairs", {}) + await async_process_repairs_platforms(hass) + client = await hass_client() + + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id="HWE-BAT_5c2fafabcdef" + ) + await hass.async_block_till_done() + + with patch("homeassistant.components.homewizard.has_v2_api", return_value=True): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Get active repair flow + issue_id = f"migrate_to_v2_api_{mock_config_entry.entry_id}" + issue = issue_registry.async_get_issue(DOMAIN, issue_id) + assert issue is not None + + assert issue.data.get("entry_id") == mock_config_entry.entry_id + + mock_homewizardenergy_v2.get_token.side_effect = DisabledError + + result = await start_repair_fix_flow(client, DOMAIN, issue_id) + + flow_id = result["flow_id"] + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm" + + result = await process_repair_fix_flow(client, flow_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "authorize" + + # Simulate user not pressing the button + result = await process_repair_fix_flow(client, flow_id, json={}) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "authorize" + assert result["errors"] == {"base": "authorization_failed"} + + # Simulate user pressing the button and getting a new token + mock_homewizardenergy_v2.get_token.side_effect = None + mock_homewizardenergy_v2.get_token.return_value = "cool_token" + result = await process_repair_fix_flow(client, flow_id, json={}) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert mock_config_entry.data[CONF_TOKEN] == "cool_token" + assert mock_config_entry.state is ConfigEntryState.LOADED + + assert issue_registry.async_get_issue(DOMAIN, issue_id) is None diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 60077c2cdf9..fe709570239 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock from homewizard_energy.errors import RequestError -from homewizard_energy.v1.models import Data import pytest from syrupy.assertion import SnapshotAssertion @@ -11,7 +10,7 @@ from homeassistant.components.homewizard.const import UPDATE_INTERVAL from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -20,6 +19,7 @@ pytestmark = [ ] +@pytest.mark.freeze_time("2025-01-28 21:45:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ("device_fixture", "entity_ids"), @@ -108,6 +108,8 @@ pytestmark = [ "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_ssid", + "sensor.device_wi_fi_strength", ], ), ( @@ -292,6 +294,22 @@ pytestmark = [ "sensor.water_meter_water", ], ), + ( + "HWE-BAT", + [ + "sensor.device_battery_cycles", + "sensor.device_current", + "sensor.device_energy_export", + "sensor.device_energy_import", + "sensor.device_frequency", + "sensor.device_power", + "sensor.device_state_of_charge", + "sensor.device_uptime", + "sensor.device_voltage", + "sensor.device_wi_fi_rssi", + "sensor.device_wi_fi_ssid", + ], + ), ], ) async def test_sensors( @@ -432,6 +450,16 @@ async def test_sensors( "sensor.device_wi_fi_strength", ], ), + ( + "HWE-BAT", + [ + "sensor.device_current", + "sensor.device_frequency", + "sensor.device_uptime", + "sensor.device_voltage", + "sensor.device_wi_fi_rssi", + ], + ), ], ) async def test_disabled_by_default_sensors( @@ -456,7 +484,7 @@ async def test_sensors_unreachable( assert (state := hass.states.get("sensor.device_energy_import_tariff_1")) assert state.state == "10830.511" - mock_homewizardenergy.data.side_effect = exception + mock_homewizardenergy.combined.side_effect = exception async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) await hass.async_block_till_done() @@ -464,15 +492,17 @@ async def test_sensors_unreachable( assert state.state == STATE_UNAVAILABLE +@pytest.mark.parametrize("exception", [RequestError]) async def test_external_sensors_unreachable( hass: HomeAssistant, mock_homewizardenergy: MagicMock, + exception: Exception, ) -> None: """Test external device sensor handles API unreachable.""" assert (state := hass.states.get("sensor.gas_meter_gas")) assert state.state == "111.111" - mock_homewizardenergy.data.return_value = Data.from_dict({}) + mock_homewizardenergy.combined.side_effect = exception async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) await hass.async_block_till_done() @@ -491,6 +521,7 @@ async def test_external_sensors_unreachable( "sensor.device_apparent_power_phase_3", "sensor.device_apparent_power", "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -520,8 +551,10 @@ async def test_external_sensors_unreachable( "sensor.device_reactive_power", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -533,6 +566,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -542,6 +576,7 @@ async def test_external_sensors_unreachable( "sensor.device_apparent_power_phase_2", "sensor.device_apparent_power_phase_3", "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -567,8 +602,10 @@ async def test_external_sensors_unreachable( "sensor.device_reactive_power_phase_3", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -579,6 +616,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -589,6 +627,7 @@ async def test_external_sensors_unreachable( "sensor.device_apparent_power_phase_3", "sensor.device_apparent_power", "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -622,7 +661,9 @@ async def test_external_sensors_unreachable( "sensor.device_reactive_power", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -633,6 +674,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -643,6 +685,7 @@ async def test_external_sensors_unreachable( "sensor.device_apparent_power_phase_3", "sensor.device_average_demand", "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -669,8 +712,10 @@ async def test_external_sensors_unreachable( "sensor.device_reactive_power_phase_3", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -681,12 +726,14 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( "SDM630", [ "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -705,8 +752,10 @@ async def test_external_sensors_unreachable( "sensor.device_power_failures_detected", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -718,6 +767,7 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( @@ -728,6 +778,7 @@ async def test_external_sensors_unreachable( "sensor.device_apparent_power_phase_3", "sensor.device_average_demand", "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -754,8 +805,10 @@ async def test_external_sensors_unreachable( "sensor.device_reactive_power_phase_3", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -766,12 +819,14 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_2", "sensor.device_voltage_swells_detected_phase_3", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", ], ), ( "HWE-KWH3", [ "sensor.device_average_demand", + "sensor.device_battery_cycles", "sensor.device_current_phase_1", "sensor.device_current_phase_2", "sensor.device_current_phase_3", @@ -790,8 +845,10 @@ async def test_external_sensors_unreachable( "sensor.device_power_failures_detected", "sensor.device_smart_meter_identifier", "sensor.device_smart_meter_model", + "sensor.device_state_of_charge", "sensor.device_tariff", "sensor.device_total_water_usage", + "sensor.device_uptime", "sensor.device_voltage_phase_1", "sensor.device_voltage_phase_2", "sensor.device_voltage_phase_3", @@ -803,6 +860,56 @@ async def test_external_sensors_unreachable( "sensor.device_voltage_swells_detected_phase_3", "sensor.device_voltage", "sensor.device_water_usage", + "sensor.device_wi_fi_rssi", + ], + ), + ( + "HWE-BAT", + [ + "sensor.device_apparent_power_phase_1", + "sensor.device_apparent_power_phase_2", + "sensor.device_apparent_power_phase_3", + "sensor.device_apparent_power", + "sensor.device_average_demand", + "sensor.device_current_phase_1", + "sensor.device_current_phase_2", + "sensor.device_current_phase_3", + "sensor.device_dsmr_version", + "sensor.device_energy_export_tariff_1", + "sensor.device_energy_export_tariff_2", + "sensor.device_energy_export_tariff_4", + "sensor.device_energy_import_tariff_1", + "sensor.device_energy_import_tariff_2", + "sensor.device_energy_import_tariff_3", + "sensor.device_energy_import_tariff_4", + "sensor.device_long_power_failures_detected", + "sensor.device_peak_demand_current_month", + "sensor.device_power_factor_phase_1", + "sensor.device_power_factor_phase_2", + "sensor.device_power_factor_phase_3", + "sensor.device_power_factor", + "sensor.device_power_failures_detected", + "sensor.device_power_phase_1", + "sensor.device_power_phase_3", + "sensor.device_reactive_power_phase_1", + "sensor.device_reactive_power_phase_2", + "sensor.device_reactive_power_phase_3", + "sensor.device_reactive_power", + "sensor.device_smart_meter_identifier", + "sensor.device_smart_meter_model", + "sensor.device_tariff", + "sensor.device_total_water_usage", + "sensor.device_voltage_phase_1", + "sensor.device_voltage_phase_2", + "sensor.device_voltage_phase_3", + "sensor.device_voltage_sags_detected_phase_1", + "sensor.device_voltage_sags_detected_phase_2", + "sensor.device_voltage_sags_detected_phase_3", + "sensor.device_voltage_swells_detected_phase_1", + "sensor.device_voltage_swells_detected_phase_2", + "sensor.device_voltage_swells_detected_phase_3", + "sensor.device_water_usage", + "sensor.device_wi_fi_strength", ], ), ], diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index d9f1ac26b4f..ae9b7653b6d 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -86,17 +86,17 @@ async def test_entities_not_created_for_device( @pytest.mark.parametrize( ("device_fixture", "entity_id", "method", "parameter"), [ - ("HWE-SKT-11", "switch.device", "state_set", "power_on"), - ("HWE-SKT-11", "switch.device_switch_lock", "state_set", "switch_lock"), - ("HWE-SKT-11", "switch.device_cloud_connection", "system_set", "cloud_enabled"), - ("HWE-SKT-21", "switch.device", "state_set", "power_on"), - ("HWE-SKT-21", "switch.device_switch_lock", "state_set", "switch_lock"), - ("HWE-SKT-21", "switch.device_cloud_connection", "system_set", "cloud_enabled"), - ("HWE-WTR", "switch.device_cloud_connection", "system_set", "cloud_enabled"), - ("SDM230", "switch.device_cloud_connection", "system_set", "cloud_enabled"), - ("SDM630", "switch.device_cloud_connection", "system_set", "cloud_enabled"), - ("HWE-KWH1", "switch.device_cloud_connection", "system_set", "cloud_enabled"), - ("HWE-KWH3", "switch.device_cloud_connection", "system_set", "cloud_enabled"), + ("HWE-SKT-11", "switch.device", "state", "power_on"), + ("HWE-SKT-11", "switch.device_switch_lock", "state", "switch_lock"), + ("HWE-SKT-11", "switch.device_cloud_connection", "system", "cloud_enabled"), + ("HWE-SKT-21", "switch.device", "state", "power_on"), + ("HWE-SKT-21", "switch.device_switch_lock", "state", "switch_lock"), + ("HWE-SKT-21", "switch.device_cloud_connection", "system", "cloud_enabled"), + ("HWE-WTR", "switch.device_cloud_connection", "system", "cloud_enabled"), + ("SDM230", "switch.device_cloud_connection", "system", "cloud_enabled"), + ("SDM630", "switch.device_cloud_connection", "system", "cloud_enabled"), + ("HWE-KWH1", "switch.device_cloud_connection", "system", "cloud_enabled"), + ("HWE-KWH3", "switch.device_cloud_connection", "system", "cloud_enabled"), ], ) async def test_switch_entities( @@ -200,9 +200,9 @@ async def test_switch_entities( @pytest.mark.parametrize( ("entity_id", "method"), [ - ("switch.device", "state"), - ("switch.device_switch_lock", "state"), - ("switch.device_cloud_connection", "system"), + ("switch.device", "combined"), + ("switch.device_switch_lock", "combined"), + ("switch.device_cloud_connection", "combined"), ], ) async def test_switch_unreachable( diff --git a/tests/components/honeywell/__init__.py b/tests/components/honeywell/__init__.py index 98fcaa551bf..94022667e0e 100644 --- a/tests/components/honeywell/__init__.py +++ b/tests/components/honeywell/__init__.py @@ -1,4 +1,4 @@ -"""Tests for honeywell component.""" +"""Tests for Honeywell component.""" from unittest.mock import MagicMock diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index e48664db9ae..dd3341aa75c 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -127,7 +127,16 @@ def device(): mock_device.refresh = AsyncMock() mock_device.heat_away_temp = HEATAWAY mock_device.cool_away_temp = COOLAWAY - + mock_device.has_humidifier = False + mock_device.has_dehumidifier = False + mock_device.humidifier_upper_limit = 60 + mock_device.humidifier_lower_limit = 10 + mock_device.humidifier_setpoint = 20 + mock_device.dehumidifier_mode = 1 + mock_device.dehumidifier_upper_limit = 55 + mock_device.dehumidifier_lower_limit = 15 + mock_device.dehumidifier_setpoint = 30 + mock_device.dehumidifier_mode = 1 mock_device.raw_dr_data = {"CoolSetpLimit": None, "HeatSetpLimit": None} return mock_device @@ -149,6 +158,8 @@ def device_with_outdoor_sensor(): mock_device.temperature_unit = "C" mock_device.outdoor_temperature = OUTDOORTEMP mock_device.outdoor_humidity = OUTDOORHUMIDITY + mock_device.has_humidifier = False + mock_device.has_dehumidifier = False mock_device.raw_ui_data = { "SwitchOffAllowed": True, "SwitchAutoAllowed": True, @@ -188,6 +199,16 @@ def another_device(): mock_device.mac_address = "macaddress1" mock_device.outdoor_temperature = None mock_device.outdoor_humidity = None + mock_device.has_humidifier = False + mock_device.has_dehumidifier = False + mock_device.humidifier_upper_limit = 60 + mock_device.humidifier_lower_limit = 10 + mock_device.humidifier_setpoint = 20 + mock_device.dehumidifier_mode = 1 + mock_device.dehumidifier_upper_limit = 55 + mock_device.dehumidifier_lower_limit = 15 + mock_device.dehumidifier_setpoint = 30 + mock_device.dehumidifier_mode = 1 mock_device.raw_ui_data = { "SwitchOffAllowed": True, "SwitchAutoAllowed": True, diff --git a/tests/components/honeywell/snapshots/test_climate.ambr b/tests/components/honeywell/snapshots/test_climate.ambr index f26064b335a..1e9958acb3f 100644 --- a/tests/components/honeywell/snapshots/test_climate.ambr +++ b/tests/components/honeywell/snapshots/test_climate.ambr @@ -12,6 +12,7 @@ ]), 'friendly_name': 'device1', 'humidity': None, + 'hvac_action': , 'hvac_modes': list([ , , diff --git a/tests/components/honeywell/snapshots/test_humidity.ambr b/tests/components/honeywell/snapshots/test_humidity.ambr new file mode 100644 index 00000000000..369167b8c1e --- /dev/null +++ b/tests/components/honeywell/snapshots/test_humidity.ambr @@ -0,0 +1,39 @@ +# serializer version: 1 +# name: test_static_attributes[dehumidifier] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 50, + 'device_class': 'dehumidifier', + 'friendly_name': 'device1 Dehumidifier', + 'humidity': 30, + 'max_humidity': 55, + 'min_humidity': 15, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.device1_dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_static_attributes[humidifier] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 50, + 'device_class': 'humidifier', + 'friendly_name': 'device1 Humidifier', + 'humidity': 20, + 'max_humidity': 60, + 'min_humidity': 10, + 'supported_features': , + }), + 'context': , + 'entity_id': 'humidifier.device1_humidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 73c5ff33dbc..7411a40e74a 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -1,4 +1,4 @@ -"""Test the Whirlpool Sixth Sense climate domain.""" +"""Test the Honeywell climate domain.""" import datetime from unittest.mock import MagicMock @@ -1200,6 +1200,7 @@ async def test_unique_id( entity_registry: er.EntityRegistry, ) -> None: """Test unique id convert to string.""" + config_entry.add_to_hass(hass) entity_registry.async_get_or_create( Platform.CLIMATE, DOMAIN, diff --git a/tests/components/honeywell/test_humidity.py b/tests/components/honeywell/test_humidity.py new file mode 100644 index 00000000000..2e1f8cec6aa --- /dev/null +++ b/tests/components/honeywell/test_humidity.py @@ -0,0 +1,110 @@ +"""Test the Honeywell humidity domain.""" + +from unittest.mock import MagicMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + DOMAIN as HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + + +async def test_humidifier_service_calls( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test the setup of the climate entities when there are no additional options available.""" + device.has_humidifier = True + await init_integration(hass, config_entry) + entity_id = f"humidifier.{device.name}_humidifier" + assert hass.states.get(f"humidifier.{device.name}_dehumidifier") is None + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_humidifier_auto.assert_called_once() + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_humidifier_off.assert_called_once() + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: entity_id, ATTR_HUMIDITY: 40}, + blocking=True, + ) + device.set_humidifier_setpoint.assert_called_once_with(40) + + +async def test_dehumidifier_service_calls( + hass: HomeAssistant, device: MagicMock, config_entry: MagicMock +) -> None: + """Test the setup of the climate entities when there are no additional options available.""" + device.has_dehumidifier = True + await init_integration(hass, config_entry) + entity_id = f"humidifier.{device.name}_dehumidifier" + assert hass.states.get(f"humidifier.{device.name}_humidifier") is None + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_dehumidifier_auto.assert_called_once() + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + device.set_dehumidifier_off.assert_called_once() + + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: entity_id, ATTR_HUMIDITY: 40}, + blocking=True, + ) + device.set_dehumidifier_setpoint.assert_called_once_with(40) + + +async def test_static_attributes( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device: MagicMock, + config_entry: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test static humidifier attributes.""" + device.has_dehumidifier = True + device.has_humidifier = True + await init_integration(hass, config_entry) + + entity_id_dehumidifier = f"humidifier.{device.name}_dehumidifier" + entity_id_humidifier = f"humidifier.{device.name}_humidifier" + entry = entity_registry.async_get(entity_id_dehumidifier) + assert entry + + state = hass.states.get(entity_id_dehumidifier) + + assert state == snapshot(name="dehumidifier") + + state = hass.states.get(entity_id_humidifier) + + assert state == snapshot(name="humidifier") diff --git a/tests/components/html5/test_config_flow.py b/tests/components/html5/test_config_flow.py index ca0b3da0389..3cde435771e 100644 --- a/tests/components/html5/test_config_flow.py +++ b/tests/components/html5/test_config_flow.py @@ -17,7 +17,7 @@ from homeassistant.components.html5.issues import ( ) from homeassistant.const import CONF_NAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -import homeassistant.helpers.issue_registry as ir +from homeassistant.helpers import issue_registry as ir MOCK_CONF = { ATTR_VAPID_EMAIL: "test@example.com", diff --git a/tests/components/html5/test_init.py b/tests/components/html5/test_init.py index 290cb381296..840890f18d1 100644 --- a/tests/components/html5/test_init.py +++ b/tests/components/html5/test_init.py @@ -1,7 +1,7 @@ """Test the HTML5 setup.""" from homeassistant.core import HomeAssistant -import homeassistant.helpers.issue_registry as ir +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 0d9388907a9..f602a8f3807 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -8,7 +8,7 @@ from unittest.mock import mock_open, patch from aiohttp.hdrs import AUTHORIZATION from aiohttp.test_utils import TestClient -import homeassistant.components.html5.notify as html5 +from homeassistant.components.html5 import notify as html5 from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 052c0031469..e31e630807e 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -192,16 +192,16 @@ async def test_cannot_access_with_trusted_ip( for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert ( - resp.status == HTTPStatus.UNAUTHORIZED - ), f"{remote_addr} shouldn't be trusted" + assert resp.status == HTTPStatus.UNAUTHORIZED, ( + f"{remote_addr} shouldn't be trusted" + ) for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert ( - resp.status == HTTPStatus.UNAUTHORIZED - ), f"{remote_addr} shouldn't be trusted" + assert resp.status == HTTPStatus.UNAUTHORIZED, ( + f"{remote_addr} shouldn't be trusted" + ) async def test_auth_active_access_with_access_token_in_header( @@ -256,16 +256,16 @@ async def test_auth_active_access_with_trusted_ip( for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert ( - resp.status == HTTPStatus.UNAUTHORIZED - ), f"{remote_addr} shouldn't be trusted" + assert resp.status == HTTPStatus.UNAUTHORIZED, ( + f"{remote_addr} shouldn't be trusted" + ) for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert ( - resp.status == HTTPStatus.UNAUTHORIZED - ), f"{remote_addr} shouldn't be trusted" + assert resp.status == HTTPStatus.UNAUTHORIZED, ( + f"{remote_addr} shouldn't be trusted" + ) async def test_auth_legacy_support_api_password_cannot_access( diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index a9a147eb17e..f75b0e7f2b0 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -13,7 +13,6 @@ import requests_mock from requests_mock import ANY from homeassistant import config_entries -from homeassistant.components import ssdp from homeassistant.components.huawei_lte.const import CONF_UNAUTHENTICATED_MODE, DOMAIN from homeassistant.const import ( CONF_NAME, @@ -25,6 +24,18 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_DEVICE_TYPE, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MANUFACTURER_URL, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_MODEL_NUMBER, + ATTR_UPNP_PRESENTATION_URL, + ATTR_UPNP_SERIAL, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -267,8 +278,8 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> "text": "Mock device", }, { - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", - ssdp.ATTR_UPNP_SERIAL: "00000000", + ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + ATTR_UPNP_SERIAL: "00000000", }, { "type": FlowResultType.FORM, @@ -283,8 +294,8 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> "text": "100002", }, { - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", - # No ssdp.ATTR_UPNP_SERIAL + ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + # No ATTR_UPNP_SERIAL }, { "type": FlowResultType.FORM, @@ -322,18 +333,18 @@ async def test_ssdp( result = await hass.config_entries.flow.async_init( DOMAIN, context=context, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="upnp:rootdevice", ssdp_location=f"{url}:60957/rootDesc.xml", upnp={ - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", - ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", - ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", - ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router", - ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678", - ssdp.ATTR_UPNP_PRESENTATION_URL: url, - ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + ATTR_UPNP_MANUFACTURER: "Huawei", + ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", + ATTR_UPNP_MODEL_NAME: "Huawei router", + ATTR_UPNP_MODEL_NUMBER: "12345678", + ATTR_UPNP_PRESENTATION_URL: url, + ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", **upnp_data, }, ), diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 692bd1405cf..e4bdda422d1 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -9,12 +9,15 @@ import pytest import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, ClientError @@ -424,13 +427,13 @@ async def test_bridge_homekit( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("0.0.0.0"), ip_addresses=[ip_address("0.0.0.0")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ) @@ -474,13 +477,13 @@ async def test_bridge_homekit_already_configured( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("0.0.0.0"), ip_addresses=[ip_address("0.0.0.0")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ) @@ -578,7 +581,7 @@ async def test_bridge_zeroconf( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.217"), ip_addresses=[ip_address("192.168.1.217")], port=443, @@ -614,7 +617,7 @@ async def test_bridge_zeroconf_already_exists( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.217"), ip_addresses=[ip_address("192.168.1.217")], port=443, @@ -639,7 +642,7 @@ async def test_bridge_zeroconf_ipv6(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("fd00::eeb5:faff:fe84:b17d"), ip_addresses=[ip_address("fd00::eeb5:faff:fe84:b17d")], port=443, @@ -687,7 +690,7 @@ async def test_bridge_connection_failed( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=443, @@ -708,13 +711,13 @@ async def test_bridge_connection_failed( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("0.0.0.0"), ip_addresses=[ip_address("0.0.0.0")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ) diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index c742124e4f0..a9fc1e5c70b 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -11,7 +11,7 @@ from homeassistant.components.light import ColorMode from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util import color +from homeassistant.util import color as color_util from .conftest import create_config_entry @@ -167,10 +167,10 @@ LIGHT_RAW = { }, "swversion": "66009461", } -LIGHT_GAMUT = color.GamutType( - color.XYPoint(0.704, 0.296), - color.XYPoint(0.2151, 0.7106), - color.XYPoint(0.138, 0.08), +LIGHT_GAMUT = color_util.GamutType( + color_util.XYPoint(0.704, 0.296), + color_util.XYPoint(0.2151, 0.7106), + color_util.XYPoint(0.138, 0.08), ) LIGHT_GAMUT_TYPE = "A" @@ -770,7 +770,7 @@ def test_hs_color() -> None: rooms={}, ) - assert light.hs_color == color.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT) + assert light.hs_color == color_util.color_xy_to_hs(0.4, 0.5, LIGHT_GAMUT) async def test_group_features( diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 2b978ffc33f..c831d40d261 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -392,7 +392,7 @@ async def test_light_availability( assert test_light is not None assert test_light.state == "on" - # Change availability by modififying the zigbee_connectivity status + # Change availability by modifying the zigbee_connectivity status for status in ("connectivity_issue", "disconnected", "connected"): mock_bridge_v2.api.emit_event( "update", diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index 388e2f68f99..7b00630f573 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -166,6 +166,7 @@ async def test_group_entity_migration_with_v1_id( ) -> None: """Test if entity schema for grouped_lights migrates from v1 to v2.""" config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + config_entry.add_to_hass(hass) # create (deviceless) entity with V1 schema in registry # using the legacy style group id as unique id @@ -201,6 +202,7 @@ async def test_group_entity_migration_with_v2_group_id( ) -> None: """Test if entity schema for grouped_lights migrates from v1 to v2.""" config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2 + config_entry.add_to_hass(hass) # create (deviceless) entity with V1 schema in registry # using the V2 group id as unique id diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 3bb1f8c2551..e1b2b2bff61 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -24,7 +24,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/hunterdouglas_powerview/const.py b/tests/components/hunterdouglas_powerview/const.py index 65b03fd5ec2..2c122ae10f2 100644 --- a/tests/components/hunterdouglas_powerview/const.py +++ b/tests/components/hunterdouglas_powerview/const.py @@ -3,32 +3,36 @@ from ipaddress import IPv4Address from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) MOCK_MAC = "AA::BB::CC::DD::EE::FF" MOCK_SERIAL = "A1B2C3D4E5G6H7" -HOMEKIT_DISCOVERY_GEN2 = zeroconf.ZeroconfServiceInfo( +HOMEKIT_DISCOVERY_GEN2 = ZeroconfServiceInfo( ip_address="1.2.3.4", ip_addresses=[IPv4Address("1.2.3.4")], hostname="mock_hostname", name="Powerview Generation 2._hap._tcp.local.", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC}, + properties={ATTR_PROPERTIES_ID: MOCK_MAC}, type="mock_type", ) -HOMEKIT_DISCOVERY_GEN3 = zeroconf.ZeroconfServiceInfo( +HOMEKIT_DISCOVERY_GEN3 = ZeroconfServiceInfo( ip_address="1.2.3.4", ip_addresses=[IPv4Address("1.2.3.4")], hostname="mock_hostname", name="Powerview Generation 3._hap._tcp.local.", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC}, + properties={ATTR_PROPERTIES_ID: MOCK_MAC}, type="mock_type", ) -ZEROCONF_DISCOVERY_GEN2 = zeroconf.ZeroconfServiceInfo( +ZEROCONF_DISCOVERY_GEN2 = ZeroconfServiceInfo( ip_address="1.2.3.4", ip_addresses=[IPv4Address("1.2.3.4")], hostname="mock_hostname", @@ -38,7 +42,7 @@ ZEROCONF_DISCOVERY_GEN2 = zeroconf.ZeroconfServiceInfo( type="mock_type", ) -ZEROCONF_DISCOVERY_GEN3 = zeroconf.ZeroconfServiceInfo( +ZEROCONF_DISCOVERY_GEN3 = ZeroconfServiceInfo( ip_address="1.2.3.4", ip_addresses=[IPv4Address("1.2.3.4")], hostname="mock_hostname", @@ -48,19 +52,19 @@ ZEROCONF_DISCOVERY_GEN3 = zeroconf.ZeroconfServiceInfo( type="mock_type", ) -DHCP_DISCOVERY_GEN2 = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY_GEN2 = DhcpServiceInfo( hostname="Powerview Generation 2", ip="1.2.3.4", macaddress="aabbccddeeff", ) -DHCP_DISCOVERY_GEN2_NO_NAME = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY_GEN2_NO_NAME = DhcpServiceInfo( hostname="", ip="1.2.3.4", macaddress="aabbccddeeff", ) -DHCP_DISCOVERY_GEN3 = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY_GEN3 = DhcpServiceInfo( hostname="Powerview Generation 3", ip="1.2.3.4", macaddress="aabbccddeeff", diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 42589bb10e0..5a48e08e5db 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -5,12 +5,13 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf from homeassistant.components.hunterdouglas_powerview.const import DOMAIN from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import DHCP_DATA, DISCOVERY_DATA, HOMEKIT_DATA, MOCK_SERIAL @@ -65,7 +66,7 @@ async def test_form_homekit_and_dhcp_cannot_connect( hass: HomeAssistant, mock_setup_entry: MagicMock, source: str, - discovery_info: dhcp.DhcpServiceInfo, + discovery_info: DhcpServiceInfo, api_version: int, ) -> None: """Test we get the form with homekit and dhcp source.""" @@ -112,7 +113,7 @@ async def test_form_homekit_and_dhcp( hass: HomeAssistant, mock_setup_entry: MagicMock, source: str, - discovery_info: dhcp.DhcpServiceInfo | zeroconf.ZeroconfServiceInfo, + discovery_info: DhcpServiceInfo | ZeroconfServiceInfo, api_version: int, ) -> None: """Test we get the form with homekit and dhcp source.""" @@ -166,10 +167,10 @@ async def test_discovered_by_homekit_and_dhcp( hass: HomeAssistant, mock_setup_entry: MagicMock, homekit_source: str, - homekit_discovery: zeroconf.ZeroconfServiceInfo, + homekit_discovery: ZeroconfServiceInfo, api_version: int, dhcp_source: str, - dhcp_discovery: dhcp.DhcpServiceInfo, + dhcp_discovery: DhcpServiceInfo, dhcp_api_version: int, ) -> None: """Test we get the form with homekit and abort for dhcp source when we get both.""" @@ -368,6 +369,7 @@ async def test_migrate_entry( version=1, minor_version=1, ) + entry.add_to_hass(hass) # Add entries with int unique_id entity_registry.async_get_or_create( @@ -387,7 +389,6 @@ async def test_migrate_entry( assert entry.version == 1 assert entry.minor_version == 1 - entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 0202cec05b9..49994e4f3ae 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -58,6 +58,15 @@ def mock_values(mower_time_zone) -> dict[str, MowerAttributes]: ) +@pytest.fixture(name="values_one_mower") +def mock_values_one_mower(mower_time_zone) -> dict[str, MowerAttributes]: + """Fixture to set correct scope for the token.""" + return mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower1.json", DOMAIN), + mower_time_zone, + ) + + @pytest.fixture def mock_config_entry(jwt: str, expires_at: int, scope: str) -> MockConfigEntry: """Return the default mocked config entry.""" @@ -119,3 +128,26 @@ def mock_automower_client(values) -> Generator[AsyncMock]: return_value=mock, ): yield mock + + +@pytest.fixture +def mock_automower_client_one_mower(values) -> Generator[AsyncMock]: + """Mock a Husqvarna Automower client.""" + + async def listen() -> None: + """Mock listen.""" + listen_block = asyncio.Event() + await listen_block.wait() + pytest.fail("Listen was not cancelled!") + + mock = AsyncMock(spec=AutomowerSession) + mock.auth = AsyncMock(side_effect=ClientWebSocketResponse) + mock.commands = AsyncMock(spec_set=_MowerCommands) + mock.get_status.return_value = values + mock.start_listening = AsyncMock(side_effect=listen) + + with patch( + "homeassistant.components.husqvarna_automower.AutomowerSession", + return_value=mock, + ): + yield mock diff --git a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr index 16d9452e847..a077eb134d4 100644 --- a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -145,6 +148,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -192,6 +196,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -238,6 +243,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/husqvarna_automower/snapshots/test_button.ambr b/tests/components/husqvarna_automower/snapshots/test_button.ambr index 2ce3aae3065..088850c1e07 100644 --- a/tests/components/husqvarna_automower/snapshots/test_button.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr b/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr index 156eee9b8df..e94eea4087c 100644 --- a/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index 036783dd6d0..1428a75d7b4 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'garden', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/husqvarna_automower/snapshots/test_number.ambr b/tests/components/husqvarna_automower/snapshots/test_number.ambr index b0ccce5800a..291aef83dbf 100644 --- a/tests/components/husqvarna_automower/snapshots/test_number.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -122,6 +124,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -178,6 +181,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index d57a829a997..02a64718276 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -260,6 +262,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -455,6 +458,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -504,6 +508,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -560,6 +565,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -614,6 +620,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -663,6 +670,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -711,6 +719,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -760,6 +769,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -809,6 +819,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -869,6 +880,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -930,6 +942,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -984,6 +997,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1038,6 +1052,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1092,6 +1107,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1146,6 +1162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1205,6 +1222,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1265,6 +1283,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1463,6 +1482,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1666,6 +1686,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1720,6 +1741,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1780,6 +1802,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/husqvarna_automower/snapshots/test_switch.ambr b/tests/components/husqvarna_automower/snapshots/test_switch.ambr index 8f8f6b367c0..5e01694e924 100644 --- a/tests/components/husqvarna_automower/snapshots/test_switch.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -282,6 +288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py index 25fa64b531f..5bef810150d 100644 --- a/tests/components/husqvarna_automower/test_button.py +++ b/tests/components/husqvarna_automower/test_button.py @@ -3,7 +3,7 @@ import datetime from unittest.mock import AsyncMock, patch -from aioautomower.exceptions import ApiException +from aioautomower.exceptions import ApiError from aioautomower.model import MowerAttributes from freezegun.api import FrozenDateTimeFactory import pytest @@ -69,7 +69,7 @@ async def test_button_states_and_commands( await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "2023-06-05T00:16:00+00:00" - getattr(mock_automower_client.commands, "error_confirm").side_effect = ApiException( + getattr(mock_automower_client.commands, "error_confirm").side_effect = ApiError( "Test error" ) with pytest.raises( @@ -111,7 +111,7 @@ async def test_sync_clock( await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "2024-02-29T11:00:00+00:00" - mock_automower_client.commands.set_datetime.side_effect = ApiException("Test error") + mock_automower_client.commands.set_datetime.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index ae688571d2c..ec1fb7391b4 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -7,10 +7,10 @@ import time from unittest.mock import AsyncMock, patch from aioautomower.exceptions import ( - ApiException, - AuthException, + ApiError, + AuthError, + HusqvarnaTimeoutError, HusqvarnaWSServerHandshakeError, - TimeoutException, ) from aioautomower.model import MowerAttributes, WorkArea from freezegun.api import FrozenDateTimeFactory @@ -111,8 +111,8 @@ async def test_expired_token_refresh_failure( @pytest.mark.parametrize( ("exception", "entry_state"), [ - (ApiException, ConfigEntryState.SETUP_RETRY), - (AuthException, ConfigEntryState.SETUP_ERROR), + (ApiError, ConfigEntryState.SETUP_RETRY), + (AuthError, ConfigEntryState.SETUP_ERROR), ], ) async def test_update_failed( @@ -142,7 +142,7 @@ async def test_update_failed( ), ( ["start_listening"], - TimeoutException, + HusqvarnaTimeoutError, "Failed to listen to websocket.", ), ], @@ -227,32 +227,79 @@ async def test_coordinator_automatic_registry_cleanup( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, values: dict[str, MowerAttributes], + freezer: FrozenDateTimeFactory, ) -> None: """Test automatic registry cleanup.""" await setup_integration(hass, mock_config_entry) entry = hass.config_entries.async_entries(DOMAIN)[0] await hass.async_block_till_done() + # Count current entitties and devices current_entites = len( er.async_entries_for_config_entry(entity_registry, entry.entry_id) ) current_devices = len( dr.async_entries_for_config_entry(device_registry, entry.entry_id) ) - - values.pop(TEST_MOWER_ID) + # Remove mower 2 and check if it worked + mower2 = values.pop("1234") mock_automower_client.get_status.return_value = values - await hass.config_entries.async_reload(mock_config_entry.entry_id) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) await hass.async_block_till_done() assert ( len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) - == current_entites - 37 + == current_entites - 12 ) assert ( len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == current_devices - 1 ) + # Add mower 2 and check if it worked + values["1234"] = mower2 + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert ( + len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) + == current_entites + ) + assert ( + len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) + == current_devices + ) + + # Remove mower 1 and check if it worked + mower1 = values.pop(TEST_MOWER_ID) + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 12 + assert ( + len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) + == current_devices - 1 + ) + # Add mower 1 and check if it worked + values[TEST_MOWER_ID] = mower1 + mock_automower_client.get_status.return_value = values + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert ( + len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) + == current_devices + ) + assert ( + len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) + == current_entites + ) async def test_add_and_remove_work_area( diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 3aca509e865..044989e5cf0 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock -from aioautomower.exceptions import ApiException +from aioautomower.exceptions import ApiError from aioautomower.model import MowerActivities, MowerAttributes, MowerStates from freezegun.api import FrozenDateTimeFactory import pytest @@ -82,7 +82,7 @@ async def test_lawn_mower_commands( getattr( mock_automower_client.commands, aioautomower_command - ).side_effect = ApiException("Test error") + ).side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -142,7 +142,7 @@ async def test_lawn_mower_service_commands( getattr( mock_automower_client.commands, aioautomower_command - ).side_effect = ApiException("Test error") + ).side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -196,7 +196,7 @@ async def test_lawn_mower_override_work_area_command( getattr( mock_automower_client.commands, aioautomower_command - ).side_effect = ApiException("Test error") + ).side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py index e1f232e7b5c..55bf5dda7eb 100644 --- a/tests/components/husqvarna_automower/test_number.py +++ b/tests/components/husqvarna_automower/test_number.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from aioautomower.exceptions import ApiException +from aioautomower.exceptions import ApiError from aioautomower.model import MowerAttributes from freezegun.api import FrozenDateTimeFactory import pytest @@ -40,7 +40,7 @@ async def test_number_commands( mocked_method = mock_automower_client.commands.set_cutting_height mocked_method.assert_called_once_with(TEST_MOWER_ID, 3) - mocked_method.side_effect = ApiException("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -84,7 +84,7 @@ async def test_number_workarea_commands( assert state.state is not None assert state.state == "75" - mocked_method.side_effect = ApiException("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py index 18d1b0ed21f..01e7607735b 100644 --- a/tests/components/husqvarna_automower/test_select.py +++ b/tests/components/husqvarna_automower/test_select.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from aioautomower.exceptions import ApiException +from aioautomower.exceptions import ApiError from aioautomower.model import HeadlightModes, MowerAttributes from freezegun.api import FrozenDateTimeFactory import pytest @@ -77,7 +77,7 @@ async def test_select_commands( mocked_method.assert_called_once_with(TEST_MOWER_ID, service.upper()) assert len(mocked_method.mock_calls) == 1 - mocked_method.side_effect = ApiException("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 100fd9fe3a4..48903a9630b 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch import zoneinfo -from aioautomower.exceptions import ApiException +from aioautomower.exceptions import ApiError from aioautomower.model import MowerAttributes, MowerModes, Zone from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory @@ -92,7 +92,7 @@ async def test_switch_commands( mocked_method = getattr(mock_automower_client.commands, aioautomower_command) mocked_method.assert_called_once_with(TEST_MOWER_ID) - mocked_method.side_effect = ApiException("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -144,12 +144,12 @@ async def test_stay_out_zone_switch_commands( freezer.tick(timedelta(seconds=EXECUTION_TIME_DELAY)) async_fire_time_changed(hass) await hass.async_block_till_done() - mocked_method.assert_called_once_with(TEST_MOWER_ID, TEST_ZONE_ID, boolean) + mocked_method.assert_called_once_with(TEST_MOWER_ID, TEST_ZONE_ID, switch=boolean) state = hass.states.get(entity_id) assert state is not None assert state.state == excepted_state - mocked_method.side_effect = ApiException("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -207,7 +207,7 @@ async def test_work_area_switch_commands( assert state is not None assert state.state == excepted_state - mocked_method.side_effect = ApiException("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", diff --git a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr index 1cc54020195..b7aa14ef0bf 100644 --- a/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower_ble/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 2de7fb1da9a..ad3a97fa6e0 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -63,7 +63,7 @@ def mock_pydrawise( controller_water_use_summary: ControllerWaterUseSummary, ) -> Generator[AsyncMock]: """Mock Hydrawise.""" - with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise: + with patch("pydrawise.hybrid.HybridClient", autospec=True) as mock_pydrawise: user.controllers = [controller] controller.sensors = sensors mock_pydrawise.return_value.get_user.return_value = user @@ -76,8 +76,8 @@ def mock_pydrawise( @pytest.fixture def mock_auth() -> Generator[AsyncMock]: - """Mock pydrawise Auth.""" - with patch("pydrawise.auth.Auth", autospec=True) as mock_auth: + """Mock pydrawise HybridAuth.""" + with patch("pydrawise.auth.HybridAuth", autospec=True) as mock_auth: yield mock_auth.return_value @@ -215,6 +215,7 @@ def mock_config_entry() -> MockConfigEntry: data={ CONF_USERNAME: "asfd@asdf.com", CONF_PASSWORD: "__password__", + CONF_API_KEY: "abc123", }, unique_id="hydrawise-customerid", version=1, diff --git a/tests/components/hydrawise/snapshots/test_binary_sensor.ambr b/tests/components/hydrawise/snapshots/test_binary_sensor.ambr index 9886345595d..84e52a7f966 100644 --- a/tests/components/hydrawise/snapshots/test_binary_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/hydrawise/snapshots/test_sensor.ambr b/tests/components/hydrawise/snapshots/test_sensor.ambr index dadf3c44789..3e475b1eeb1 100644 --- a/tests/components/hydrawise/snapshots/test_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -61,6 +62,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -165,6 +168,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -220,6 +224,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -275,6 +280,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -324,6 +330,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -372,6 +379,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -420,6 +428,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -476,6 +485,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -525,6 +535,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -573,6 +584,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/hydrawise/snapshots/test_switch.ambr b/tests/components/hydrawise/snapshots/test_switch.ambr index 977bd15f004..9ad37ddbfbf 100644 --- a/tests/components/hydrawise/snapshots/test_switch.ambr +++ b/tests/components/hydrawise/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/hydrawise/snapshots/test_valve.ambr b/tests/components/hydrawise/snapshots/test_valve.ambr index cac08893324..197e7796a07 100644 --- a/tests/components/hydrawise/snapshots/test_valve.ambr +++ b/tests/components/hydrawise/snapshots/test_valve.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,6 +56,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/hydrawise/test_config_flow.py b/tests/components/hydrawise/test_config_flow.py index 4d25fd5840b..594286b7f01 100644 --- a/tests/components/hydrawise/test_config_flow.py +++ b/tests/components/hydrawise/test_config_flow.py @@ -33,21 +33,26 @@ async def test_form( assert result["step_id"] == "user" assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}, + { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + }, ) mock_pydrawise.get_user.return_value = user await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Hydrawise" - assert result2["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "asdf@asdf.com" + assert result["data"] == { CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", } assert len(mock_setup_entry.mock_calls) == 1 - mock_auth.token.assert_awaited_once_with() + mock_auth.check.assert_awaited_once_with() mock_pydrawise.get_user.assert_awaited_once_with(fetch_zones=False) @@ -60,7 +65,11 @@ async def test_form_api_error( init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} + data = { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + } result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) @@ -69,19 +78,26 @@ async def test_form_api_error( mock_pydrawise.get_user.reset_mock(side_effect=True) mock_pydrawise.get_user.return_value = user - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] is FlowResultType.CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_form_auth_connect_timeout( hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock ) -> None: - """Test we handle API errors.""" - mock_auth.token.side_effect = TimeoutError + """Test we handle connection timeout errors.""" + mock_auth.check.side_effect = TimeoutError init_result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, + context={ + "source": config_entries.SOURCE_USER, + }, ) - data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} + data = { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + } result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) @@ -89,9 +105,9 @@ async def test_form_auth_connect_timeout( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "timeout_connect"} - mock_auth.token.reset_mock(side_effect=True) - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] is FlowResultType.CREATE_ENTRY + mock_auth.check.reset_mock(side_effect=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_form_client_connect_timeout( @@ -102,7 +118,11 @@ async def test_form_client_connect_timeout( init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} + data = { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + } result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) @@ -112,29 +132,33 @@ async def test_form_client_connect_timeout( mock_pydrawise.get_user.reset_mock(side_effect=True) mock_pydrawise.get_user.return_value = user - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] is FlowResultType.CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_form_not_authorized_error( hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock ) -> None: """Test we handle API errors.""" - mock_auth.token.side_effect = NotAuthorizedError + mock_auth.check.side_effect = NotAuthorizedError init_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"} + data = { + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + } result = await hass.config_entries.flow.async_configure( init_result["flow_id"], data ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} - mock_auth.token.reset_mock(side_effect=True) - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) - assert result2["type"] is FlowResultType.CREATE_ENTRY + mock_auth.check.reset_mock(side_effect=True) + result = await hass.config_entries.flow.async_configure(result["flow_id"], data) + assert result["type"] is FlowResultType.CREATE_ENTRY async def test_reauth( @@ -148,7 +172,9 @@ async def test_reauth( title="Hydrawise", domain=DOMAIN, data={ - CONF_API_KEY: "__api_key__", + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "bad-password", + CONF_API_KEY: "__api-key__", }, unique_id="hydrawise-12345", ) @@ -160,14 +186,62 @@ async def test_reauth( flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 [result] = flows - assert result["step_id"] == "user" + assert result["step_id"] == "reauth_confirm" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}, - ) mock_pydrawise.get_user.return_value = user + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + }, + ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_fails( + hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock, user: User +) -> None: + """Test that the reauth flow handles API errors.""" + mock_config_entry = MockConfigEntry( + title="Hydrawise", + domain=DOMAIN, + data={ + CONF_USERNAME: "asdf@asdf.com", + CONF_PASSWORD: "bad-password", + CONF_API_KEY: "__api-key__", + }, + unique_id="hydrawise-12345", + ) + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["step_id"] == "reauth_confirm" + + mock_auth.check.side_effect = NotAuthorizedError + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_auth.check.reset_mock(side_effect=True) + mock_pydrawise.get_user.return_value = user + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "__password__", + CONF_API_KEY: "__api-key__", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 4109fe0f653..ac7e6c25b0d 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -10,7 +10,6 @@ from unittest.mock import AsyncMock, Mock, patch from hyperion import const -from homeassistant.components import ssdp from homeassistant.components.hyperion.const import ( CONF_AUTH_ID, CONF_CREATE_TOKEN, @@ -30,6 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from . import ( TEST_AUTH_REQUIRED_RESP, @@ -67,7 +67,7 @@ TEST_REQUEST_TOKEN_FAIL = { "error": "Token request timeout or denied", } -TEST_SSDP_SERVICE_INFO = ssdp.SsdpServiceInfo( +TEST_SSDP_SERVICE_INFO = SsdpServiceInfo( ssdp_st="upnp:rootdevice", ssdp_location=f"http://{TEST_HOST}:{TEST_PORT_UI}/description.xml", ssdp_usn=f"uuid:{TEST_SYSINFO_ID}", diff --git a/tests/components/idasen_desk/conftest.py b/tests/components/idasen_desk/conftest.py index 24ef8311445..20cc95e8ae4 100644 --- a/tests/components/idasen_desk/conftest.py +++ b/tests/components/idasen_desk/conftest.py @@ -19,9 +19,14 @@ def mock_bluetooth(enable_bluetooth: None) -> Generator[None]: @pytest.fixture(autouse=False) def mock_desk_api(): """Set up idasen desk API fixture.""" - with mock.patch( - "homeassistant.components.idasen_desk.coordinator.Desk" - ) as desk_patched: + with ( + mock.patch( + "homeassistant.components.idasen_desk.coordinator.Desk" + ) as desk_patched, + mock.patch( + "homeassistant.components.idasen_desk.config_flow.Desk", new=desk_patched + ), + ): mock_desk = MagicMock() def mock_init( @@ -33,17 +38,20 @@ def mock_desk_api(): desk_patched.side_effect = mock_init - async def mock_connect(ble_device): + async def mock_connect(ble_device, retry: bool = True): mock_desk.is_connected = True - mock_desk.trigger_update_callback(None) + if mock_desk.trigger_update_callback: + mock_desk.trigger_update_callback(None) async def mock_disconnect(): mock_desk.is_connected = False - mock_desk.trigger_update_callback(None) + if mock_desk.trigger_update_callback: + mock_desk.trigger_update_callback(None) async def mock_move_to(height: float): mock_desk.height_percent = height - mock_desk.trigger_update_callback(height) + if mock_desk.trigger_update_callback: + mock_desk.trigger_update_callback(height) async def mock_move_up(): await mock_move_to(100) diff --git a/tests/components/idasen_desk/test_buttons.py b/tests/components/idasen_desk/test_button.py similarity index 100% rename from tests/components/idasen_desk/test_buttons.py rename to tests/components/idasen_desk/test_button.py diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index c27cdea58aa..15baac1b055 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -1,6 +1,6 @@ """Test the IKEA Idasen Desk config flow.""" -from unittest.mock import ANY, patch +from unittest.mock import ANY, MagicMock, patch from bleak.exc import BleakError from idasen_ha.errors import AuthFailedError @@ -17,7 +17,7 @@ from . import IDASEN_DISCOVERY_INFO, NOT_IDASEN_DISCOVERY_INFO from tests.common import MockConfigEntry -async def test_user_step_success(hass: HomeAssistant) -> None: +async def test_user_step_success(hass: HomeAssistant, mock_desk_api: MagicMock) -> None: """Test user step success path.""" with patch( "homeassistant.components.idasen_desk.config_flow.async_discovered_service_info", @@ -30,14 +30,52 @@ async def test_user_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with ( - patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), - patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), - patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry, + with patch( + "homeassistant.components.idasen_desk.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == IDASEN_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_replaces_ignored_device( + hass: HomeAssistant, mock_desk_api: MagicMock +) -> None: + """Test user step replaces ignored devices.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=IDASEN_DISCOVERY_INFO.address, + source=config_entries.SOURCE_IGNORE, + data={CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.idasen_desk.config_flow.async_discovered_service_info", + return_value=[NOT_IDASEN_DISCOVERY_INFO, IDASEN_DISCOVERY_INFO], ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.idasen_desk.async_setup_entry", return_value=True + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -89,9 +127,20 @@ async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: assert result["reason"] == "no_devices_found" -@pytest.mark.parametrize("exception", [TimeoutError(), BleakError()]) +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (TimeoutError, "cannot_connect"), + (BleakError, "cannot_connect"), + (AuthFailedError, "auth_failed"), + (RuntimeError, "unknown"), + ], +) async def test_user_step_cannot_connect( - hass: HomeAssistant, exception: Exception + hass: HomeAssistant, + mock_desk_api: MagicMock, + exception: Exception, + expected_error: str, ) -> None: """Test user step with a cannot connect error.""" with patch( @@ -105,90 +154,26 @@ async def test_user_step_cannot_connect( assert result["step_id"] == "user" assert result["errors"] == {} - with ( - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - side_effect=exception, - ), - patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - }, - ) - await hass.async_block_till_done() + default_connect_side_effect = mock_desk_api.connect.side_effect + mock_desk_api.connect.side_effect = exception + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": expected_error} - with ( - patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), - patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), - patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == IDASEN_DISCOVERY_INFO.name - assert result3["data"] == { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - } - assert result3["result"].unique_id == IDASEN_DISCOVERY_INFO.address - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_user_step_auth_failed(hass: HomeAssistant) -> None: - """Test user step with an auth failed error.""" + mock_desk_api.connect.side_effect = default_connect_side_effect with patch( - "homeassistant.components.idasen_desk.config_flow.async_discovered_service_info", - return_value=[IDASEN_DISCOVERY_INFO], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with ( - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - side_effect=AuthFailedError, - ), - patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "auth_failed"} - - with ( - patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), - patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), - patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -206,70 +191,9 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: - """Test user step with an unknown exception.""" - with patch( - "homeassistant.components.idasen_desk.config_flow.async_discovered_service_info", - return_value=[NOT_IDASEN_DISCOVERY_INFO, IDASEN_DISCOVERY_INFO], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - with ( - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - side_effect=RuntimeError, - ), - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect", - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} - - with ( - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - ), - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect", - ), - patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == IDASEN_DISCOVERY_INFO.name - assert result3["data"] == { - CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address, - } - assert result3["result"].unique_id == IDASEN_DISCOVERY_INFO.address - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_bluetooth_step_success(hass: HomeAssistant) -> None: +async def test_bluetooth_step_success( + hass: HomeAssistant, mock_desk_api: MagicMock +) -> None: """Test bluetooth step success path.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -280,16 +204,10 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with ( - patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect" - ) as desk_connect, - patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), - patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): + with patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -305,4 +223,4 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: } assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address assert len(mock_setup_entry.mock_calls) == 1 - desk_connect.assert_called_with(ANY, retry=False) + mock_desk_api.connect.assert_called_with(ANY, retry=False) diff --git a/tests/components/idasen_desk/test_sensors.py b/tests/components/idasen_desk/test_sensor.py similarity index 100% rename from tests/components/idasen_desk/test_sensors.py rename to tests/components/idasen_desk/test_sensor.py diff --git a/tests/components/igloohome/__init__.py b/tests/components/igloohome/__init__.py new file mode 100644 index 00000000000..835414b2825 --- /dev/null +++ b/tests/components/igloohome/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the igloohome integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the igloohome integration for testing.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/igloohome/conftest.py b/tests/components/igloohome/conftest.py new file mode 100644 index 00000000000..d630f5af7cb --- /dev/null +++ b/tests/components/igloohome/conftest.py @@ -0,0 +1,72 @@ +"""Common fixtures for the igloohome tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from igloohome_api import GetDeviceInfoResponse, GetDevicesResponse +import pytest + +from homeassistant.components.igloohome.const import DOMAIN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +GET_DEVICE_INFO_RESPONSE_LOCK = GetDeviceInfoResponse( + id="123456", + type="Lock", + deviceId="OE1X123cbb11", + deviceName="Front Door", + pairedAt="2024-11-09T11:19:25+00:00", + homeId=[], + linkedDevices=[], + batteryLevel=100, +) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.igloohome.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +async def mock_auth() -> Generator[AsyncMock]: + """Set up the mock usages of the igloohome_api.Auth class. Defaults to always successfully operate.""" + with patch( + "homeassistant.components.igloohome.config_flow.IgloohomeAuth.async_get_access_token", + return_value="mock_access_token", + ) as mock_auth: + yield mock_auth + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Client Credentials", + domain=DOMAIN, + version=1, + data={CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-secret"}, + ) + + +@pytest.fixture(autouse=True) +def mock_api() -> Generator[AsyncMock]: + """Set up the Api module. Defaults to always returning a single lock.""" + with ( + patch( + "homeassistant.components.igloohome.IgloohomeApi", + autospec=True, + ) as api_mock, + ): + api = api_mock.return_value + api.get_devices.return_value = GetDevicesResponse( + nextCursor="", + payload=[GET_DEVICE_INFO_RESPONSE_LOCK], + ) + api.get_device_info.return_value = GET_DEVICE_INFO_RESPONSE_LOCK + yield api diff --git a/tests/components/igloohome/snapshots/test_sensor.ambr b/tests/components/igloohome/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..9e17343d4fa --- /dev/null +++ b/tests/components/igloohome/snapshots/test_sensor.ambr @@ -0,0 +1,50 @@ +# serializer version: 1 +# name: test_sensors[sensor.front_door_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.front_door_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'igloohome', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'battery_OE1X123cbb11', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.front_door_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Front Door Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.front_door_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- diff --git a/tests/components/igloohome/test_config_flow.py b/tests/components/igloohome/test_config_flow.py new file mode 100644 index 00000000000..1f7656d88d6 --- /dev/null +++ b/tests/components/igloohome/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the igloohome config flow.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock + +from aiohttp import ClientError +from igloohome_api import AuthException +import pytest + +from homeassistant import config_entries +from homeassistant.components.igloohome.const import DOMAIN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import setup_integration + +FORM_USER_INPUT = { + CONF_CLIENT_ID: "client-id", + CONF_CLIENT_SECRET: "client-secret", +} + + +async def test_form_valid_input( + hass: HomeAssistant, + mock_setup_entry: Generator[AsyncMock], + mock_auth: Generator[AsyncMock], +) -> None: + """Test that the form correct reacts to valid input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + FORM_USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Client Credentials" + assert result["data"] == FORM_USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "result_error"), + [(AuthException(), "invalid_auth"), (ClientError(), "cannot_connect")], +) +async def test_form_invalid_input( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_auth: Generator[AsyncMock], + exception: Exception, + result_error: str, +) -> None: + """Tests where we handle errors in the config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_auth.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + FORM_USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": result_error} + + # Make sure the config flow tests finish with either an + # FlowResultType.CREATE_ENTRY or FlowResultType.ABORT so + # we can show the config flow is able to recover from an error. + mock_auth.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + FORM_USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Client Credentials" + assert result["data"] == FORM_USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_abort_on_matching_entry( + hass: HomeAssistant, + mock_config_entry: Generator[AsyncMock], + mock_auth: Generator[AsyncMock], +) -> None: + """Tests where we handle errors in the config flow.""" + # Create first config flow. + await setup_integration(hass, mock_config_entry) + + # Attempt another config flow with the same client credentials + # and ensure that FlowResultType.ABORT is returned. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + FORM_USER_INPUT, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/igloohome/test_sensor.py b/tests/components/igloohome/test_sensor.py new file mode 100644 index 00000000000..bfc60574450 --- /dev/null +++ b/tests/components/igloohome/test_sensor.py @@ -0,0 +1,26 @@ +"""Test sensors for igloohome integration.""" + +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the igloohome sensors.""" + with patch("homeassistant.components.igloohome.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index c26eae28086..2f946459bfe 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -32,7 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/image/conftest.py b/tests/components/image/conftest.py index 06ef7db9f49..6879bc793bb 100644 --- a/tests/components/image/conftest.py +++ b/tests/components/image/conftest.py @@ -7,7 +7,10 @@ import pytest from homeassistant.components import image from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -123,7 +126,7 @@ class MockImageConfigEntry: self, hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test image platform via config entry.""" async_add_entities([self._entities]) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 3e7c8f2fb91..6ff6d925d7e 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -6,8 +6,7 @@ from unittest.mock import PropertyMock, patch import pytest -from homeassistant.components import http -import homeassistant.components.image_processing as ip +from homeassistant.components import http, image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index d4281b9e513..b86855bd78f 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -171,11 +171,8 @@ async def test_receiving_message_successfully( assert data["subject"] == "Test subject" assert data["uid"] == "1" assert "Test body" in data["text"] - assert ( - valid_date - and isinstance(data["date"], datetime) - or not valid_date - and data["date"] is None + assert (valid_date and isinstance(data["date"], datetime)) or ( + not valid_date and data["date"] is None ) @@ -581,11 +578,8 @@ async def test_reset_last_message( assert data["subject"] == "Test subject" assert data["text"] assert data["initial"] - assert ( - valid_date - and isinstance(data["date"], datetime) - or not valid_date - and data["date"] is None + assert (valid_date and isinstance(data["date"], datetime)) or ( + not valid_date and data["date"] is None ) # Simulate an update where no messages are found (needed for pushed coordinator) diff --git a/tests/components/imgw_pib/conftest.py b/tests/components/imgw_pib/conftest.py index 6f23ed3ee80..a10b9b54532 100644 --- a/tests/components/imgw_pib/conftest.py +++ b/tests/components/imgw_pib/conftest.py @@ -16,11 +16,11 @@ HYDROLOGICAL_DATA = HydrologicalData( river="River Name", station_id="123", water_level=SensorData(name="Water Level", value=526.0), - flood_alarm_level=SensorData(name="Flood Alarm Level", value=630.0), - flood_warning_level=SensorData(name="Flood Warning Level", value=590.0), + flood_alarm_level=SensorData(name="Flood Alarm Level", value=None), + flood_warning_level=SensorData(name="Flood Warning Level", value=None), water_temperature=SensorData(name="Water Temperature", value=10.8), - flood_alarm=False, - flood_warning=False, + flood_alarm=None, + flood_warning=None, water_level_measurement_date=datetime(2024, 4, 27, 10, 0, tzinfo=UTC), water_temperature_measurement_date=datetime(2024, 4, 27, 10, 10, tzinfo=UTC), ) diff --git a/tests/components/imgw_pib/snapshots/test_diagnostics.ambr b/tests/components/imgw_pib/snapshots/test_diagnostics.ambr index f15fc706d7e..97453930c1e 100644 --- a/tests/components/imgw_pib/snapshots/test_diagnostics.ambr +++ b/tests/components/imgw_pib/snapshots/test_diagnostics.ambr @@ -22,17 +22,17 @@ 'version': 1, }), 'hydrological_data': dict({ - 'flood_alarm': False, + 'flood_alarm': None, 'flood_alarm_level': dict({ 'name': 'Flood Alarm Level', 'unit': None, - 'value': 630.0, + 'value': None, }), - 'flood_warning': False, + 'flood_warning': None, 'flood_warning_level': dict({ 'name': 'Flood Warning Level', 'unit': None, - 'value': 590.0, + 'value': None, }), 'river': 'River Name', 'station': 'Station Name', diff --git a/tests/components/imgw_pib/snapshots/test_sensor.ambr b/tests/components/imgw_pib/snapshots/test_sensor.ambr index 6c69b890842..ccc6e46befa 100644 --- a/tests/components/imgw_pib/snapshots/test_sensor.ambr +++ b/tests/components/imgw_pib/snapshots/test_sensor.ambr @@ -1,108 +1,4 @@ # serializer version: 1 -# name: test_sensor[sensor.river_name_station_name_flood_alarm_level-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.river_name_station_name_flood_alarm_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Flood alarm level', - 'platform': 'imgw_pib', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'flood_alarm_level', - 'unique_id': '123_flood_alarm_level', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.river_name_station_name_flood_alarm_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'distance', - 'friendly_name': 'River Name (Station Name) Flood alarm level', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.river_name_station_name_flood_alarm_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '630.0', - }) -# --- -# name: test_sensor[sensor.river_name_station_name_flood_warning_level-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.river_name_station_name_flood_warning_level', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Flood warning level', - 'platform': 'imgw_pib', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'flood_warning_level', - 'unique_id': '123_flood_warning_level', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor[sensor.river_name_station_name_flood_warning_level-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'distance', - 'friendly_name': 'River Name (Station Name) Flood warning level', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.river_name_station_name_flood_warning_level', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '590.0', - }) -# --- # name: test_sensor[sensor.river_name_station_name_water_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -112,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -167,6 +64,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/imgw_pib/test_binary_sensor.py b/tests/components/imgw_pib/test_binary_sensor.py deleted file mode 100644 index 185d4b18575..00000000000 --- a/tests/components/imgw_pib/test_binary_sensor.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Test the IMGW-PIB binary sensor platform.""" - -from unittest.mock import AsyncMock, patch - -from freezegun.api import FrozenDateTimeFactory -from imgw_pib import ApiError -from syrupy import SnapshotAssertion - -from homeassistant.components.imgw_pib.const import UPDATE_INTERVAL -from homeassistant.const import STATE_UNAVAILABLE, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import init_integration - -from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform - -ENTITY_ID = "binary_sensor.river_name_station_name_flood_alarm" - - -async def test_binary_sensor( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, - mock_imgw_pib_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test states of the binary sensor.""" - with patch("homeassistant.components.imgw_pib.PLATFORMS", [Platform.BINARY_SENSOR]): - await init_integration(hass, mock_config_entry) - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -async def test_availability( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_imgw_pib_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure that we mark the entities unavailable correctly when service is offline.""" - await init_integration(hass, mock_config_entry) - - state = hass.states.get(ENTITY_ID) - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "off" - - mock_imgw_pib_client.get_hydrological_data.side_effect = ApiError("API Error") - freezer.tick(UPDATE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state - assert state.state == STATE_UNAVAILABLE - - mock_imgw_pib_client.get_hydrological_data.side_effect = None - freezer.tick(UPDATE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "off" diff --git a/tests/components/imgw_pib/test_init.py b/tests/components/imgw_pib/test_init.py index e1b7cda7c88..e352c643676 100644 --- a/tests/components/imgw_pib/test_init.py +++ b/tests/components/imgw_pib/test_init.py @@ -4,9 +4,11 @@ from unittest.mock import AsyncMock, patch from imgw_pib import ApiError +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_PLATFORM from homeassistant.components.imgw_pib.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from . import init_integration @@ -43,3 +45,27 @@ async def test_unload_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_remove_binary_sensor_entity( + hass: HomeAssistant, + mock_imgw_pib_client: AsyncMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test removing a binary_sensor entity.""" + entity_id = "binary_sensor.river_name_station_name_flood_alarm" + mock_config_entry.add_to_hass(hass) + + entity_registry.async_get_or_create( + BINARY_SENSOR_PLATFORM, + DOMAIN, + "123_flood_alarm", + suggested_object_id=entity_id.rsplit(".", maxsplit=1)[-1], + config_entry=mock_config_entry, + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) is None diff --git a/tests/components/imgw_pib/test_sensor.py b/tests/components/imgw_pib/test_sensor.py index 276c021fad5..a1920f38006 100644 --- a/tests/components/imgw_pib/test_sensor.py +++ b/tests/components/imgw_pib/test_sensor.py @@ -7,7 +7,8 @@ from imgw_pib import ApiError import pytest from syrupy import SnapshotAssertion -from homeassistant.components.imgw_pib.const import UPDATE_INTERVAL +from homeassistant.components.imgw_pib.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -65,3 +66,27 @@ async def test_availability( assert state assert state.state != STATE_UNAVAILABLE assert state.state == "526.0" + + +async def test_remove_entity( + hass: HomeAssistant, + mock_imgw_pib_client: AsyncMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test removing entity.""" + entity_id = "sensor.river_name_station_name_flood_alarm_level" + mock_config_entry.add_to_hass(hass) + + entity_registry.async_get_or_create( + SENSOR_PLATFORM, + DOMAIN, + "123_flood_alarm_level", + suggested_object_id=entity_id.rsplit(".", maxsplit=1)[-1], + config_entry=mock_config_entry, + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id) is None diff --git a/tests/components/improv_ble/test_config_flow.py b/tests/components/improv_ble/test_config_flow.py index 2df4be2ba7d..4536c64349c 100644 --- a/tests/components/improv_ble/test_config_flow.py +++ b/tests/components/improv_ble/test_config_flow.py @@ -664,6 +664,6 @@ async def test_provision_fails_invalid_data( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_improv_data" assert ( - "Aborting improv flow, device AA:BB:CC:DD:EE:F0 sent invalid improv data: '000000000000'" + "Received invalid improv via BLE data '000000000000' from device with bluetooth address 'AA:BB:CC:DD:EE:F0'" in caplog.text ) diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index b00e3a638c8..aacfa886f52 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -8,7 +8,6 @@ from incomfortclient import DisplayCode import pytest from homeassistant.components.incomfort.const import DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -19,8 +18,13 @@ MOCK_CONFIG = { "password": "verysecret", } +MOCK_CONFIG_DHCP = { + "username": "admin", + "password": "verysecret", +} + MOCK_HEATER_STATUS = { - "display_code": DisplayCode(126), + "display_code": DisplayCode.STANDBY, "display_text": "standby", "fault_code": None, "is_burning": False, @@ -36,6 +40,23 @@ MOCK_HEATER_STATUS = { "rfstatus_cntr": 0, } +MOCK_HEATER_STATUS_HEATING = { + "display_code": DisplayCode.OPENTHERM, + "display_text": "opentherm", + "fault_code": None, + "is_burning": True, + "is_failed": False, + "is_pumping": True, + "is_tapping": False, + "heater_temp": 35.34, + "tap_temp": 30.21, + "pressure": 1.86, + "serial_no": "c0ffeec0ffee", + "nodenr": 249, + "rf_message_rssi": 30, + "rfstatus_cntr": 0, +} + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -53,12 +74,22 @@ def mock_entry_data() -> dict[str, Any]: return MOCK_CONFIG +@pytest.fixture +def mock_entry_options() -> dict[str, Any] | None: + """Mock config entry options for fixture.""" + return None + + @pytest.fixture def mock_config_entry( - hass: HomeAssistant, mock_entry_data: dict[str, Any] -) -> ConfigEntry: + hass: HomeAssistant, + mock_entry_data: dict[str, Any], + mock_entry_options: dict[str, Any], +) -> MockConfigEntry: """Mock a config entry setup for incomfort integration.""" - entry = MockConfigEntry(domain=DOMAIN, data=mock_entry_data) + entry = MockConfigEntry( + domain=DOMAIN, data=mock_entry_data, options=mock_entry_options + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/incomfort/snapshots/test_binary_sensor.ambr b/tests/components/incomfort/snapshots/test_binary_sensor.ambr index 2f2319b6a44..518ea230705 100644 --- a/tests/components/incomfort/snapshots/test_binary_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_binary_sensor.ambr @@ -6,11 +6,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_burner', 'has_entity_name': True, 'hidden_by': None, @@ -53,11 +54,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_fault', 'has_entity_name': True, 'hidden_by': None, @@ -101,11 +103,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_hot_water_tap', 'has_entity_name': True, 'hidden_by': None, @@ -148,11 +151,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_pump', 'has_entity_name': True, 'hidden_by': None, @@ -195,11 +199,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_burner', 'has_entity_name': True, 'hidden_by': None, @@ -242,11 +247,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_fault', 'has_entity_name': True, 'hidden_by': None, @@ -290,11 +296,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_hot_water_tap', 'has_entity_name': True, 'hidden_by': None, @@ -337,11 +344,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_pump', 'has_entity_name': True, 'hidden_by': None, @@ -384,11 +392,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_burner', 'has_entity_name': True, 'hidden_by': None, @@ -431,11 +440,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_fault', 'has_entity_name': True, 'hidden_by': None, @@ -479,11 +489,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_hot_water_tap', 'has_entity_name': True, 'hidden_by': None, @@ -526,11 +537,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_pump', 'has_entity_name': True, 'hidden_by': None, @@ -573,11 +585,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_burner', 'has_entity_name': True, 'hidden_by': None, @@ -620,11 +633,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_fault', 'has_entity_name': True, 'hidden_by': None, @@ -668,11 +682,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_hot_water_tap', 'has_entity_name': True, 'hidden_by': None, @@ -715,11 +730,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_pump', 'has_entity_name': True, 'hidden_by': None, @@ -762,11 +778,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_burner', 'has_entity_name': True, 'hidden_by': None, @@ -809,11 +826,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_fault', 'has_entity_name': True, 'hidden_by': None, @@ -857,11 +875,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_hot_water_tap', 'has_entity_name': True, 'hidden_by': None, @@ -904,11 +923,12 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.boiler_pump', 'has_entity_name': True, 'hidden_by': None, diff --git a/tests/components/incomfort/snapshots/test_climate.ambr b/tests/components/incomfort/snapshots/test_climate.ambr index 17adcbb3bab..df3fe3f710b 100644 --- a/tests/components/incomfort/snapshots/test_climate.ambr +++ b/tests/components/incomfort/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-entry] +# name: test_setup_platform[legacy-override][climate.thermostat_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,11 +12,12 @@ 'min_temp': 5.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'climate', - 'entity_category': None, + 'entity_category': , 'entity_id': 'climate.thermostat_1', 'has_entity_name': True, 'hidden_by': None, @@ -38,7 +39,74 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-state] +# name: test_setup_platform[legacy-override][climate.thermostat_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 21.4, + 'friendly_name': 'Thermostat 1', + 'hvac_action': , + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + 'status': dict({ + 'override': 19.0, + 'room_temp': 21.42, + 'setpoint': 18.0, + }), + 'supported_features': , + 'temperature': 18.0, + }), + 'context': , + 'entity_id': 'climate.thermostat_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_setup_platform[legacy-zero_override][climate.thermostat_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': , + 'entity_id': 'climate.thermostat_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[legacy-zero_override][climate.thermostat_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 21.4, @@ -65,7 +133,7 @@ 'state': 'heat', }) # --- -# name: test_setup_platform[new_thermostat][climate.thermostat_1-entry] +# name: test_setup_platform[modern-override][climate.thermostat_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -78,11 +146,12 @@ 'min_temp': 5.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'climate', - 'entity_category': None, + 'entity_category': , 'entity_id': 'climate.thermostat_1', 'has_entity_name': True, 'hidden_by': None, @@ -104,7 +173,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_platform[new_thermostat][climate.thermostat_1-state] +# name: test_setup_platform[modern-override][climate.thermostat_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 21.4, @@ -116,7 +185,74 @@ 'max_temp': 30.0, 'min_temp': 5.0, 'status': dict({ - 'override': 18.0, + 'override': 19.0, + 'room_temp': 21.42, + 'setpoint': 18.0, + }), + 'supported_features': , + 'temperature': 19.0, + }), + 'context': , + 'entity_id': 'climate.thermostat_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_setup_platform[modern-zero_override][climate.thermostat_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': , + 'entity_id': 'climate.thermostat_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'incomfort', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'c0ffeec0ffee_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_platform[modern-zero_override][climate.thermostat_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 21.4, + 'friendly_name': 'Thermostat 1', + 'hvac_action': , + 'hvac_modes': list([ + , + ]), + 'max_temp': 30.0, + 'min_temp': 5.0, + 'status': dict({ + 'override': 0.0, 'room_temp': 21.42, 'setpoint': 18.0, }), diff --git a/tests/components/incomfort/snapshots/test_diagnostics.ambr b/tests/components/incomfort/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..e7c99f37acd --- /dev/null +++ b/tests/components/incomfort/snapshots/test_diagnostics.ambr @@ -0,0 +1,35 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config': dict({ + 'host': '192.168.1.12', + 'password': '**REDACTED**', + 'username': 'admin', + }), + 'gateway': dict({ + 'heater_0': dict({ + 'display_code': 126, + 'display_text': 'standby', + 'fault_code': None, + 'heater_temp': 35.34, + 'is_burning': False, + 'is_failed': False, + 'is_pumping': False, + 'is_tapping': False, + 'nodenr': 249, + 'pressure': 1.86, + 'rf_message_rssi': 30, + 'rfstatus_cntr': 0, + 'rooms': dict({ + '0': dict({ + 'override': 18.0, + 'room_temp': 21.42, + 'setpoint': 18.0, + }), + }), + 'serial_no': 'c0ffeec0ffee', + 'tap_temp': 30.21, + }), + }), + }) +# --- diff --git a/tests/components/incomfort/snapshots/test_sensor.ambr b/tests/components/incomfort/snapshots/test_sensor.ambr index 8c9ea60f455..294a6094164 100644 --- a/tests/components/incomfort/snapshots/test_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_sensor.ambr @@ -8,11 +8,12 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.boiler_pressure', 'has_entity_name': True, 'hidden_by': None, @@ -59,11 +60,12 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.boiler_tap_temperature', 'has_entity_name': True, 'hidden_by': None, @@ -111,11 +113,12 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.boiler_temperature', 'has_entity_name': True, 'hidden_by': None, diff --git a/tests/components/incomfort/snapshots/test_water_heater.ambr b/tests/components/incomfort/snapshots/test_water_heater.ambr index 06b0d0c1e52..d3fc2b057fc 100644 --- a/tests/components/incomfort/snapshots/test_water_heater.ambr +++ b/tests/components/incomfort/snapshots/test_water_heater.ambr @@ -9,11 +9,12 @@ 'min_temp': 30.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'water_heater', - 'entity_category': None, + 'entity_category': , 'entity_id': 'water_heater.boiler', 'has_entity_name': True, 'hidden_by': None, diff --git a/tests/components/incomfort/test_binary_sensor.py b/tests/components/incomfort/test_binary_sensor.py index c724cf4b7b2..e90cc3ac391 100644 --- a/tests/components/incomfort/test_binary_sensor.py +++ b/tests/components/incomfort/test_binary_sensor.py @@ -17,6 +17,7 @@ from tests.common import snapshot_platform @patch("homeassistant.components.incomfort.PLATFORMS", [Platform.BINARY_SENSOR]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_platform( hass: HomeAssistant, mock_incomfort: MagicMock, @@ -45,6 +46,7 @@ async def test_setup_platform( ids=["is_failed", "is_pumping", "is_burning", "is_tapping"], ) @patch("homeassistant.components.incomfort.PLATFORMS", [Platform.BINARY_SENSOR]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_binary_sensors_alt( hass: HomeAssistant, mock_incomfort: MagicMock, diff --git a/tests/components/incomfort/test_climate.py b/tests/components/incomfort/test_climate.py index ae4c1cf31f7..dbcf14e3bd7 100644 --- a/tests/components/incomfort/test_climate.py +++ b/tests/components/incomfort/test_climate.py @@ -1,15 +1,19 @@ """Climate sensor tests for Intergas InComfort integration.""" -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from syrupy import SnapshotAssertion +from homeassistant.components import climate +from homeassistant.components.incomfort.coordinator import InComfortData from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from .conftest import MOCK_HEATER_STATUS, MOCK_HEATER_STATUS_HEATING + from tests.common import snapshot_platform @@ -17,10 +21,15 @@ from tests.common import snapshot_platform @pytest.mark.parametrize( "mock_room_status", [ - {"room_temp": 21.42, "setpoint": 18.0, "override": 18.0}, + {"room_temp": 21.42, "setpoint": 18.0, "override": 19.0}, {"room_temp": 21.42, "setpoint": 18.0, "override": 0.0}, ], - ids=["new_thermostat", "legacy_thermostat"], + ids=["override", "zero_override"], +) +@pytest.mark.parametrize( + "mock_entry_options", + [None, {"legacy_setpoint_status": True}], + ids=["modern", "legacy"], ) async def test_setup_platform( hass: HomeAssistant, @@ -31,8 +40,54 @@ async def test_setup_platform( ) -> None: """Test the incomfort entities are set up correctly. - Legacy thermostats report 0.0 as override if no override is set, - but new thermostat sync the override with the actual setpoint instead. + Thermostats report 0.0 as override if no override is set + or when the setpoint has been changed manually, + Some older thermostats do not reset the override setpoint has been changed manually. """ await hass.config_entries.async_setup(mock_config_entry.entry_id) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("mock_heater_status", "hvac_action"), + [ + (MOCK_HEATER_STATUS.copy(), climate.HVACAction.IDLE), + (MOCK_HEATER_STATUS_HEATING.copy(), climate.HVACAction.HEATING), + ], + ids=["idle", "heating"], +) +async def test_hvac_state( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: ConfigEntry, + hvac_action: climate.HVACAction, +) -> None: + """Test the HVAC state of the thermostat.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + state = hass.states.get("climate.thermostat_1") + assert state is not None + assert state.attributes["hvac_action"] is hvac_action + + +async def test_target_temp( + hass: HomeAssistant, mock_incomfort: MagicMock, mock_config_entry: ConfigEntry +) -> None: + """Test changing the target temperature.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + state = hass.states.get("climate.thermostat_1") + assert state is not None + + incomfort_data: InComfortData = mock_config_entry.runtime_data.incomfort_data + + with patch.object( + incomfort_data.heaters[0].rooms[0], "set_override", AsyncMock() + ) as mock_set_override: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_TEMPERATURE, + service_data={ + ATTR_ENTITY_ID: "climate.thermostat_1", + ATTR_TEMPERATURE: 19.0, + }, + ) + mock_set_override.assert_called_once_with(19.0) diff --git a/tests/components/incomfort/test_config_flow.py b/tests/components/incomfort/test_config_flow.py index 287fd85715f..e3579182b3d 100644 --- a/tests/components/incomfort/test_config_flow.py +++ b/tests/components/incomfort/test_config_flow.py @@ -1,21 +1,36 @@ """Tests for the Intergas InComfort config flow.""" -from unittest.mock import AsyncMock, MagicMock +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientResponseError -from incomfortclient import IncomfortError, InvalidHeaterList +from incomfortclient import InvalidGateway, InvalidHeaterList import pytest from homeassistant.components.incomfort.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from .conftest import MOCK_CONFIG +from .conftest import MOCK_CONFIG, MOCK_CONFIG_DHCP from tests.common import MockConfigEntry +DHCP_SERVICE_INFO = DhcpServiceInfo( + hostname="rfgateway", + ip="192.168.1.12", + macaddress="0004A3DEADFF", +) + +DHCP_SERVICE_INFO_ALT = DhcpServiceInfo( + hostname="rfgateway", + ip="192.168.1.99", + macaddress="0004A3DEADFF", +) + async def test_form( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_incomfort: MagicMock @@ -38,7 +53,9 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 -async def test_entry_already_configured(hass: HomeAssistant) -> None: +async def test_entry_already_configured( + hass: HomeAssistant, mock_incomfort: MagicMock +) -> None: """Test aborting if the entry is already configured.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) entry.add_to_hass(hass) @@ -64,24 +81,22 @@ async def test_entry_already_configured(hass: HomeAssistant) -> None: ("exc", "error", "base"), [ ( - IncomfortError(ClientResponseError(None, None, status=401)), + InvalidGateway, "auth_error", - CONF_PASSWORD, - ), - ( - IncomfortError(ClientResponseError(None, None, status=404)), - "not_found", "base", ), ( - IncomfortError(ClientResponseError(None, None, status=500)), + InvalidHeaterList, + "no_heaters", + "base", + ), + ( + ClientResponseError(None, None, status=500), "unknown", "base", ), - (IncomfortError, "unknown", "base"), - (ValueError, "unknown", "base"), (TimeoutError, "timeout_error", "base"), - (InvalidHeaterList, "no_heaters", "base"), + (ValueError, "unknown", "base"), ], ) async def test_form_validation( @@ -113,3 +128,276 @@ async def test_form_validation( ) assert result["type"] is FlowResultType.CREATE_ENTRY assert "errors" not in result + + +async def test_dhcp_flow_simple( + hass: HomeAssistant, + mock_incomfort: MagicMock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test dhcp flow for older gateway without authentication needed. + + Assert on the creation of the gateway device, climate and boiler devices. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_SERVICE_INFO + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "dhcp_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == {"host": "192.168.1.12"} + + config_entry: ConfigEntry = result["result"] + entry_id = config_entry.entry_id + + await hass.async_block_till_done(wait_background_tasks=True) + + # Check the gateway device is discovered + gateway_device = device_registry.async_get_device(identifiers={(DOMAIN, entry_id)}) + assert gateway_device is not None + assert gateway_device.name == "RFGateway" + assert gateway_device.manufacturer == "Intergas" + assert gateway_device.connections == {("mac", "00:04:a3:de:ad:ff")} + + devices = device_registry.devices.get_devices_for_config_entry_id(entry_id) + assert len(devices) == 3 + boiler_device = device_registry.async_get_device( + identifiers={(DOMAIN, "c0ffeec0ffee")} + ) + assert boiler_device.via_device_id == gateway_device.id + assert boiler_device is not None + climate_device = device_registry.async_get_device( + identifiers={(DOMAIN, "c0ffeec0ffee_1")} + ) + assert climate_device is not None + assert climate_device.via_device_id == gateway_device.id + + # Check the host is dynamically updated + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_SERVICE_INFO_ALT + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == DHCP_SERVICE_INFO_ALT.ip + + +async def test_dhcp_flow_migrates_existing_entry_without_unique_id( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test dhcp flow migrates an existing entry without unique_id.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_SERVICE_INFO + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Check the gateway device is discovered after a reload + # And has updated connections + gateway_device = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert gateway_device is not None + assert gateway_device.name == "RFGateway" + assert gateway_device.manufacturer == "Intergas" + assert gateway_device.connections == {("mac", "00:04:a3:de:ad:ff")} + + devices = device_registry.devices.get_devices_for_config_entry_id( + mock_config_entry.entry_id + ) + assert len(devices) == 3 + boiler_device = device_registry.async_get_device( + identifiers={(DOMAIN, "c0ffeec0ffee")} + ) + assert boiler_device.via_device_id == gateway_device.id + assert boiler_device is not None + climate_device = device_registry.async_get_device( + identifiers={(DOMAIN, "c0ffeec0ffee_1")} + ) + assert climate_device is not None + assert climate_device.via_device_id == gateway_device.id + + +async def test_dhcp_flow_wih_auth( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_incomfort: MagicMock +) -> None: + """Test dhcp flow for with authentication.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_SERVICE_INFO + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "dhcp_confirm" + + # Try again, but now with the correct host, but still with an auth error + with patch.object( + mock_incomfort(), + "heaters", + side_effect=InvalidGateway, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "192.168.1.12"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "dhcp_auth" + assert result["errors"] == {"base": "auth_error"} + + # Submit the form with added credentials + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG_DHCP + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Intergas InComfort/Intouch Lan2RF gateway" + assert result["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow_success( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the re-authentication flow succeeds.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "new-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_flow_failure( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the re-authentication flow fails.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch.object( + mock_incomfort(), + "heaters", + side_effect=InvalidGateway, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "incorrect-password"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "auth_error"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "new-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reconfigure_flow_success( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the re-configure flow succeeds.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG | {CONF_PASSWORD: "new-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_flow_failure( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the re-configure flow fails.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + with patch.object( + mock_incomfort(), + "heaters", + side_effect=InvalidGateway, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG | {CONF_PASSWORD: "wrong-password"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "auth_error"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG | {CONF_PASSWORD: "new-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +@pytest.mark.parametrize( + ("user_input", "legacy_setpoint_status"), + [ + ({}, False), + ({"legacy_setpoint_status": False}, False), + ({"legacy_setpoint_status": True}, True), + ], +) +async def test_options_flow( + hass: HomeAssistant, + mock_incomfort: MagicMock, + user_input: dict[str, Any], + legacy_setpoint_status: bool, +) -> None: + """Test options flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + with patch("homeassistant.components.incomfort.async_setup_entry") as restart_mock: + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + await hass.async_block_till_done(wait_background_tasks=True) + assert restart_mock.call_count == 1 + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"] == {"legacy_setpoint_status": legacy_setpoint_status} + assert entry.options.get("legacy_setpoint_status", False) is legacy_setpoint_status diff --git a/tests/components/incomfort/test_diagnostics.py b/tests/components/incomfort/test_diagnostics.py new file mode 100644 index 00000000000..02493681705 --- /dev/null +++ b/tests/components/incomfort/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Test diagnostics for the Intergas InComfort integration.""" + +from unittest.mock import MagicMock + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, SnapshotAssertion +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the incomfort integration diagnostics.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + snapshot.assert_match( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + ) diff --git a/tests/components/incomfort/test_init.py b/tests/components/incomfort/test_init.py index 0390a47a616..92ce0afa448 100644 --- a/tests/components/incomfort/test_init.py +++ b/tests/components/incomfort/test_init.py @@ -1,33 +1,89 @@ """Tests for Intergas InComfort integration.""" from datetime import timedelta -from unittest.mock import MagicMock, patch +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch -from aiohttp import ClientResponseError +from aiohttp import ClientResponseError, RequestInfo from freezegun.api import FrozenDateTimeFactory -from incomfortclient import IncomfortError +from incomfortclient import InvalidGateway, InvalidHeaterList import pytest +from homeassistant.components.incomfort import DOMAIN from homeassistant.components.incomfort.coordinator import UPDATE_INTERVAL from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import DeviceRegistry -from tests.common import async_fire_time_changed +from .conftest import MOCK_HEATER_STATUS + +from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_setup_platforms( hass: HomeAssistant, mock_incomfort: MagicMock, entity_registry: er.EntityRegistry, - mock_config_entry: ConfigEntry, + mock_config_entry: MockConfigEntry, ) -> None: """Test the incomfort integration is set up correctly.""" await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.LOADED +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + "mock_heater_status", [MOCK_HEATER_STATUS | {"serial_no": "c01d00c0ffee"}] +) +async def test_stale_devices_cleanup( + hass: HomeAssistant, + device_registry: DeviceRegistry, + mock_incomfort: MagicMock, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_heater_status: dict[str, Any], +) -> None: + """Test the incomfort integration is cleaning up stale devices.""" + # Setup an old heater with serial_no c01d00c0ffee + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.LOADED + await hass.config_entries.async_unload(mock_config_entry.entry_id) + old_entries = device_registry.devices.get_devices_for_config_entry_id( + mock_config_entry.entry_id + ) + assert len(old_entries) == 3 + old_heater = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee")}) + assert old_heater is not None + assert old_heater.serial_number == "c01d00c0ffee" + old_climate = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee_1")}) + assert old_heater is not None + old_climate = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee_1")}) + assert old_climate is not None + + mock_heater_status["serial_no"] = "c0ffeec0ffee" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.LOADED + + new_entries = device_registry.devices.get_devices_for_config_entry_id( + mock_config_entry.entry_id + ) + assert len(new_entries) == 3 + new_heater = device_registry.async_get_device({(DOMAIN, "c0ffeec0ffee")}) + assert new_heater is not None + assert new_heater.serial_number == "c0ffeec0ffee" + new_climate = device_registry.async_get_device({(DOMAIN, "c0ffeec0ffee_1")}) + assert new_climate is not None + + old_heater = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee")}) + assert old_heater is None + old_climate = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee_1")}) + assert old_climate is None + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_coordinator_updates( hass: HomeAssistant, mock_incomfort: MagicMock, @@ -59,12 +115,31 @@ async def test_coordinator_updates( assert state.state == "1.84" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( "exc", [ - IncomfortError(ClientResponseError(None, None, status=401)), - IncomfortError(ClientResponseError(None, None, status=500)), - IncomfortError(ValueError("some_error")), + ClientResponseError( + RequestInfo( + url="http://example.com", + method="GET", + headers=[], + real_url="http://example.com", + ), + None, + status=401, + ), + InvalidHeaterList, + ClientResponseError( + RequestInfo( + url="http://example.com", + method="GET", + headers=[], + real_url="http://example.com", + ), + None, + status=500, + ), TimeoutError, ], ) @@ -91,3 +166,60 @@ async def test_coordinator_update_fails( state = hass.states.get("sensor.boiler_pressure") assert state is not None assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("exc", "config_entry_state"), + [ + ( + InvalidGateway, + ConfigEntryState.SETUP_ERROR, + ), + (InvalidHeaterList, ConfigEntryState.SETUP_RETRY), + ( + ClientResponseError( + RequestInfo( + url="http://example.com", + method="GET", + headers=[], + real_url="http://example.com", + ), + None, + status=404, + ), + ConfigEntryState.SETUP_ERROR, + ), + ( + ClientResponseError( + RequestInfo( + url="http://example.com", + method="GET", + headers=[], + real_url="http://example.com", + ), + None, + status=500, + ), + ConfigEntryState.SETUP_RETRY, + ), + (TimeoutError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_entry_setup_fails( + hass: HomeAssistant, + mock_incomfort: MagicMock, + freezer: FrozenDateTimeFactory, + mock_config_entry: ConfigEntry, + exc: Exception, + config_entry_state: ConfigEntryState, +) -> None: + """Test the incomfort coordinator entry setup fails.""" + with patch( + "homeassistant.components.incomfort.async_connect_gateway", + AsyncMock(side_effect=exc), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + state = hass.states.get("sensor.boiler_pressure") + assert state is None + assert mock_config_entry.state is config_entry_state diff --git a/tests/components/incomfort/test_sensor.py b/tests/components/incomfort/test_sensor.py index d01fd9b403e..df0db39a56c 100644 --- a/tests/components/incomfort/test_sensor.py +++ b/tests/components/incomfort/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch +import pytest from syrupy import SnapshotAssertion from homeassistant.config_entries import ConfigEntry @@ -12,6 +13,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @patch("homeassistant.components.incomfort.PLATFORMS", [Platform.SENSOR]) async def test_setup_platform( hass: HomeAssistant, diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index 154132c34fc..796f57da55b 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -75,6 +75,36 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=SPS_SERVICE_INFO.address, + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 8105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 411f084d39a..7d491f0cdcd 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -217,6 +217,34 @@ async def test_set_datetime_3(hass: HomeAssistant) -> None: assert state.attributes["timestamp"] == dt_obj.timestamp() +async def test_set_datetime_4(hass: HomeAssistant) -> None: + """Test set_datetime method using timestamp 0.""" + await async_setup_component( + hass, DOMAIN, {DOMAIN: {"test_datetime": {"has_time": True, "has_date": True}}} + ) + + entity_id = "input_datetime.test_datetime" + + dt_obj = datetime.datetime( + 1969, 12, 31, 16, 00, 00, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) + + await async_set_timestamp(hass, entity_id, 0) + + state = hass.states.get(entity_id) + assert state.state == dt_obj.strftime(FORMAT_DATETIME) + assert state.attributes["has_time"] + assert state.attributes["has_date"] + + assert state.attributes["year"] == 1969 + assert state.attributes["month"] == 12 + assert state.attributes["day"] == 31 + assert state.attributes["hour"] == 16 + assert state.attributes["minute"] == 00 + assert state.attributes["second"] == 0 + assert state.attributes["timestamp"] == 0 + + async def test_set_datetime_time(hass: HomeAssistant) -> None: """Test set_datetime method with only time.""" await async_setup_component( diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 31d38a603f1..33e71be6dc2 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -8,7 +8,6 @@ import pytest from voluptuous_serialize import convert from homeassistant import config_entries -from homeassistant.components import dhcp, usb from homeassistant.components.insteon.config_flow import ( STEP_HUB_V1, STEP_HUB_V2, @@ -20,6 +19,8 @@ from homeassistant.config_entries import ConfigEntryState, ConfigFlowResult from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.usb import UsbServiceInfo from .const import ( MOCK_DEVICE, @@ -209,7 +210,7 @@ async def test_form_select_hub_v2(hass: HomeAssistant) -> None: async def test_form_discovery_dhcp(hass: HomeAssistant) -> None: """Test the discovery of the Hub via DHCP.""" - discovery_info = dhcp.DhcpServiceInfo("1.2.3.4", "", "aabbccddeeff") + discovery_info = DhcpServiceInfo("1.2.3.4", "", "aabbccddeeff") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=discovery_info ) @@ -270,7 +271,7 @@ async def test_failed_connection_hub(hass: HomeAssistant) -> None: async def test_discovery_via_usb(hass: HomeAssistant) -> None: """Test usb flow.""" - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyINSTEON", pid="AAAA", vid="AAAA", @@ -302,7 +303,7 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE: "/dev/ttyUSB1"}} ).add_to_hass(hass) - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyINSTEON", pid="AAAA", vid="AAAA", diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 974c8bb8691..ba4a6bdf198 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -28,7 +28,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, @@ -843,6 +843,39 @@ async def test_on_valid_source_expect_update_on_time( assert float(state.state) < 1.8 +async def test_on_0_source_expect_0_and_update_when_source_gets_positive( + hass: HomeAssistant, +) -> None: + """Test whether time based integration updates the integral on a valid zero source.""" + start_time = dt_util.utcnow() + + with freeze_time(start_time) as freezer: + await _setup_integral_sensor(hass, max_sub_interval=DEFAULT_MAX_SUB_INTERVAL) + await _update_source_sensor(hass, 0) + await hass.async_block_till_done() + + # wait one minute and one second + freezer.tick(61) + async_fire_time_changed(hass, dt_util.now()) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + + assert condition.async_numeric_state(hass, state) is True + assert float(state.state) == 0 # integral is 0 after integration of 0 + + # wait one second and update state + freezer.tick(1) + async_fire_time_changed(hass, dt_util.now()) + await _update_source_sensor(hass, 100) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + + # approx 100*1/3600 (right method after 1 second since last integration) + assert 0.027 < float(state.state) < 0.029 + + async def test_on_unvailable_source_expect_no_update_on_time( hass: HomeAssistant, ) -> None: diff --git a/tests/components/intellifire/snapshots/test_binary_sensor.ambr b/tests/components/intellifire/snapshots/test_binary_sensor.ambr index 1b85db51d68..afa3c1fa8a9 100644 --- a/tests/components/intellifire/snapshots/test_binary_sensor.ambr +++ b/tests/components/intellifire/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -246,6 +251,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +300,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -341,6 +348,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -389,6 +397,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -437,6 +446,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -485,6 +495,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -533,6 +544,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -581,6 +593,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -629,6 +642,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -676,6 +690,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -724,6 +739,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -771,6 +787,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/intellifire/snapshots/test_climate.ambr b/tests/components/intellifire/snapshots/test_climate.ambr index 36f719d2264..d0744424cff 100644 --- a/tests/components/intellifire/snapshots/test_climate.ambr +++ b/tests/components/intellifire/snapshots/test_climate.ambr @@ -14,6 +14,7 @@ 'target_temp_step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/intellifire/snapshots/test_sensor.ambr b/tests/components/intellifire/snapshots/test_sensor.ambr index d749da216ac..548c8d5a8aa 100644 --- a/tests/components/intellifire/snapshots/test_sensor.ambr +++ b/tests/components/intellifire/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -101,6 +103,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -200,6 +204,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -248,6 +253,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -297,6 +303,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -349,6 +356,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -401,6 +409,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -450,6 +459,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index f1465c4dcd4..96d0fa17e63 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import AsyncMock from intellifire4py.exceptions import LoginError from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.intellifire.const import CONF_SERIAL, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -166,7 +166,7 @@ async def test_dhcp_discovery_intellifire_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="zentrios-Test", @@ -196,7 +196,7 @@ async def test_dhcp_discovery_non_intellifire_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="zentrios-Evil", diff --git a/tests/components/iometer/__init__.py b/tests/components/iometer/__init__.py new file mode 100644 index 00000000000..5c08438925e --- /dev/null +++ b/tests/components/iometer/__init__.py @@ -0,0 +1 @@ +"""Tests for the IOmeter integration.""" diff --git a/tests/components/iometer/conftest.py b/tests/components/iometer/conftest.py new file mode 100644 index 00000000000..ee45021952e --- /dev/null +++ b/tests/components/iometer/conftest.py @@ -0,0 +1,57 @@ +"""Common fixtures for the IOmeter tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from iometer import Reading, Status +import pytest + +from homeassistant.components.iometer.const import DOMAIN +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.iometer.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_iometer_client() -> Generator[AsyncMock]: + """Mock a new IOmeter client.""" + with ( + patch( + "homeassistant.components.iometer.IOmeterClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.iometer.config_flow.IOmeterClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.host = "10.0.0.2" + client.get_current_reading.return_value = Reading.from_json( + load_fixture("reading.json", DOMAIN) + ) + client.get_current_status.return_value = Status.from_json( + load_fixture("status.json", DOMAIN) + ) + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a IOmeter config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="IOmeter-1ISK0000000000", + data={CONF_HOST: "10.0.0.2"}, + unique_id="658c2b34-2017-45f2-a12b-731235f8bb97", + ) diff --git a/tests/components/iometer/fixtures/reading.json b/tests/components/iometer/fixtures/reading.json new file mode 100644 index 00000000000..82190c88883 --- /dev/null +++ b/tests/components/iometer/fixtures/reading.json @@ -0,0 +1,14 @@ +{ + "__typename": "iometer.reading.v1", + "meter": { + "number": "1ISK0000000000", + "reading": { + "time": "2024-11-11T11:11:11Z", + "registers": [ + { "obis": "01-00:01.08.00*ff", "value": 1234.5, "unit": "Wh" }, + { "obis": "01-00:02.08.00*ff", "value": 5432.1, "unit": "Wh" }, + { "obis": "01-00:10.07.00*ff", "value": 100, "unit": "W" } + ] + } + } +} diff --git a/tests/components/iometer/fixtures/status.json b/tests/components/iometer/fixtures/status.json new file mode 100644 index 00000000000..4d3001d8454 --- /dev/null +++ b/tests/components/iometer/fixtures/status.json @@ -0,0 +1,19 @@ +{ + "__typename": "iometer.status.v1", + "meter": { + "number": "1ISK0000000000" + }, + "device": { + "bridge": { "rssi": -30, "version": "build-65" }, + "id": "658c2b34-2017-45f2-a12b-731235f8bb97", + "core": { + "connectionStatus": "connected", + "rssi": -30, + "version": "build-58", + "powerStatus": "battery", + "batteryLevel": 100, + "attachmentStatus": "attached", + "pinStatus": "entered" + } + } +} diff --git a/tests/components/iometer/test_config_flow.py b/tests/components/iometer/test_config_flow.py new file mode 100644 index 00000000000..49fce459282 --- /dev/null +++ b/tests/components/iometer/test_config_flow.py @@ -0,0 +1,171 @@ +"""Test the IOmeter config flow.""" + +from ipaddress import ip_address +from unittest.mock import AsyncMock + +from iometer import IOmeterConnectionError + +from homeassistant.components import zeroconf +from homeassistant.components.iometer.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +IP_ADDRESS = "10.0.0.2" +IOMETER_DEVICE_ID = "658c2b34-2017-45f2-a12b-731235f8bb97" + +ZEROCONF_DISCOVERY = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address(IP_ADDRESS), + ip_addresses=[ip_address(IP_ADDRESS)], + hostname="IOmeter-EC63E8.local.", + name="IOmeter-EC63E8", + port=80, + type="_iometer._tcp.", + properties={}, +) + + +async def test_user_flow( + hass: HomeAssistant, + mock_iometer_client: AsyncMock, +) -> None: + """Test full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: IP_ADDRESS}, + ) + + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "IOmeter 1ISK0000000000" + assert result["data"] == {CONF_HOST: IP_ADDRESS} + assert result["result"].unique_id == IOMETER_DEVICE_ID + + +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_iometer_client: AsyncMock, +) -> None: + """Test zeroconf flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "zeroconf_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "IOmeter 1ISK0000000000" + assert result["data"] == {CONF_HOST: IP_ADDRESS} + assert result["result"].unique_id == IOMETER_DEVICE_ID + + +async def test_zeroconf_flow_abort_duplicate( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test zeroconf flow aborts with duplicate.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_flow_connection_error( + hass: HomeAssistant, + mock_iometer_client: AsyncMock, +) -> None: + """Test zeroconf flow.""" + mock_iometer_client.get_current_status.side_effect = IOmeterConnectionError() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_user_flow_connection_error( + hass: HomeAssistant, + mock_iometer_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test flow error.""" + mock_iometer_client.get_current_status.side_effect = IOmeterConnectionError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: IP_ADDRESS}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + mock_iometer_client.get_current_status.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: IP_ADDRESS}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_flow_abort_duplicate( + hass: HomeAssistant, + mock_iometer_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: IP_ADDRESS}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/iotty/conftest.py b/tests/components/iotty/conftest.py index 1935a069cca..1ce645b402e 100644 --- a/tests/components/iotty/conftest.py +++ b/tests/components/iotty/conftest.py @@ -6,10 +6,12 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientSession from iottycloud.device import Device from iottycloud.lightswitch import LightSwitch +from iottycloud.outlet import Outlet from iottycloud.shutter import Shutter from iottycloud.verbs import ( LS_DEVICE_TYPE_UID, OPEN_PERCENTAGE, + OU_DEVICE_TYPE_UID, RESULT, SH_DEVICE_TYPE_UID, STATUS, @@ -73,6 +75,22 @@ test_sh_one_added = [ sh_2, ] +ou_0 = Outlet("TestOU", "TEST_SERIAL_OU_0", OU_DEVICE_TYPE_UID, "[TEST] Outlet 0") + +ou_1 = Outlet("TestOU1", "TEST_SERIAL_OU_1", OU_DEVICE_TYPE_UID, "[TEST] Outlet 1") + +ou_2 = Outlet("TestOU2", "TEST_SERIAL_OU_2", OU_DEVICE_TYPE_UID, "[TEST] Outlet 2") + +test_ou = [ou_0, ou_1] + +test_ou_one_removed = [ou_0] + +test_ou_one_added = [ + ou_0, + ou_1, + ou_2, +] + @pytest.fixture async def local_oauth_impl(hass: HomeAssistant): @@ -151,7 +169,7 @@ def mock_iotty() -> Generator[MagicMock]: def mock_coordinator() -> Generator[MagicMock]: """Mock IottyDataUpdateCoordinator.""" with patch( - "homeassistant.components.iotty.coordinator.IottyDataUpdateCoordinator", + "homeassistant.components.iotty.IottyDataUpdateCoordinator", autospec=True, ) as coordinator_mock: yield coordinator_mock @@ -175,6 +193,16 @@ def mock_get_devices_twolightswitches() -> Generator[AsyncMock]: yield mock_fn +@pytest.fixture +def mock_get_devices_two_outlets() -> Generator[AsyncMock]: + """Mock for get_devices, returning two outlets.""" + + with patch( + "iottycloud.cloudapi.CloudApi.get_devices", return_value=test_ou + ) as mock_fn: + yield mock_fn + + @pytest.fixture def mock_get_devices_twoshutters() -> Generator[AsyncMock]: """Mock for get_devices, returning two shutters.""" diff --git a/tests/components/iotty/snapshots/test_switch.ambr b/tests/components/iotty/snapshots/test_switch.ambr index 8ec22ed162a..16913d340f0 100644 --- a/tests/components/iotty/snapshots/test_switch.ambr +++ b/tests/components/iotty/snapshots/test_switch.ambr @@ -15,6 +15,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -120,6 +122,19 @@ 'switch.test_light_switch_2_test_serial_2', ]) # --- +# name: test_outlet_insertion_ok + list([ + 'switch.test_outlet_0_test_serial_ou_0', + 'switch.test_outlet_1_test_serial_ou_1', + ]) +# --- +# name: test_outlet_insertion_ok.1 + list([ + 'switch.test_outlet_0_test_serial_ou_0', + 'switch.test_outlet_1_test_serial_ou_1', + 'switch.test_outlet_2_test_serial_ou_2', + ]) +# --- # name: test_setup_entry_ok_nodevices list([ ]) diff --git a/tests/components/iotty/test_switch.py b/tests/components/iotty/test_switch.py index 235a897c305..069fa665cac 100644 --- a/tests/components/iotty/test_switch.py +++ b/tests/components/iotty/test_switch.py @@ -20,12 +20,52 @@ from homeassistant.helpers import ( entity_registry as er, ) -from .conftest import test_ls_one_added, test_ls_one_removed +from .conftest import test_ls_one_added, test_ls_one_removed, test_ou_one_added from tests.common import MockConfigEntry, async_fire_time_changed -async def test_turn_on_ok( +async def check_command_ok( + entity_id: str, + initial_status: str, + final_status: str, + command: str, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + local_oauth_impl: ClientSession, + mock_get_status, + mock_command_fn, +) -> None: + """Issue a command.""" + + mock_config_entry.add_to_hass(hass) + + config_entry_oauth2_flow.async_register_implementation( + hass, DOMAIN, local_oauth_impl + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert (state := hass.states.get(entity_id)) + assert state.state == initial_status + + mock_get_status.return_value = {RESULT: {STATUS: final_status}} + + await hass.services.async_call( + SWITCH_DOMAIN, + command, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + await hass.async_block_till_done() + mock_command_fn.assert_called_once() + + assert (state := hass.states.get(entity_id)) + assert state.state == final_status + + +async def test_turn_on_light_ok( hass: HomeAssistant, mock_config_entry: MockConfigEntry, local_oauth_impl: ClientSession, @@ -37,34 +77,45 @@ async def test_turn_on_ok( entity_id = "switch.test_light_switch_0_test_serial_0" - mock_config_entry.add_to_hass(hass) - - config_entry_oauth2_flow.async_register_implementation( - hass, DOMAIN, local_oauth_impl + await check_command_ok( + entity_id=entity_id, + initial_status=STATUS_OFF, + final_status=STATUS_ON, + command=SERVICE_TURN_ON, + hass=hass, + mock_config_entry=mock_config_entry, + local_oauth_impl=local_oauth_impl, + mock_get_status=mock_get_status_filled_off, + mock_command_fn=mock_command_fn, ) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert (state := hass.states.get(entity_id)) - assert state.state == STATUS_OFF +async def test_turn_on_outlet_ok( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + local_oauth_impl: ClientSession, + mock_get_devices_two_outlets, + mock_get_status_filled_off, + mock_command_fn, +) -> None: + """Issue a turnon command.""" - mock_get_status_filled_off.return_value = {RESULT: {STATUS: STATUS_ON}} + entity_id = "switch.test_outlet_0_test_serial_ou_0" - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, + await check_command_ok( + entity_id=entity_id, + initial_status=STATUS_OFF, + final_status=STATUS_ON, + command=SERVICE_TURN_ON, + hass=hass, + mock_config_entry=mock_config_entry, + local_oauth_impl=local_oauth_impl, + mock_get_status=mock_get_status_filled_off, + mock_command_fn=mock_command_fn, ) - await hass.async_block_till_done() - mock_command_fn.assert_called_once() - assert (state := hass.states.get(entity_id)) - assert state.state == STATUS_ON - - -async def test_turn_off_ok( +async def test_turn_off_light_ok( hass: HomeAssistant, mock_config_entry: MockConfigEntry, local_oauth_impl: ClientSession, @@ -76,32 +127,43 @@ async def test_turn_off_ok( entity_id = "switch.test_light_switch_0_test_serial_0" - mock_config_entry.add_to_hass(hass) - - config_entry_oauth2_flow.async_register_implementation( - hass, DOMAIN, local_oauth_impl + await check_command_ok( + entity_id=entity_id, + initial_status=STATUS_ON, + final_status=STATUS_OFF, + command=SERVICE_TURN_OFF, + hass=hass, + mock_config_entry=mock_config_entry, + local_oauth_impl=local_oauth_impl, + mock_get_status=mock_get_status_filled, + mock_command_fn=mock_command_fn, ) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert (state := hass.states.get(entity_id)) - assert state.state == STATUS_ON +async def test_turn_off_outlet_ok( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + local_oauth_impl: ClientSession, + mock_get_devices_two_outlets, + mock_get_status_filled, + mock_command_fn, +) -> None: + """Issue a turnoff command.""" - mock_get_status_filled.return_value = {RESULT: {STATUS: STATUS_OFF}} + entity_id = "switch.test_outlet_0_test_serial_ou_0" - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, + await check_command_ok( + entity_id=entity_id, + initial_status=STATUS_ON, + final_status=STATUS_OFF, + command=SERVICE_TURN_OFF, + hass=hass, + mock_config_entry=mock_config_entry, + local_oauth_impl=local_oauth_impl, + mock_get_status=mock_get_status_filled, + mock_command_fn=mock_command_fn, ) - await hass.async_block_till_done() - mock_command_fn.assert_called_once() - - assert (state := hass.states.get(entity_id)) - assert state.state == STATUS_OFF - async def test_setup_entry_ok_nodevices( hass: HomeAssistant, @@ -229,6 +291,40 @@ async def test_devices_insertion_ok( assert hass.states.async_entity_ids() == snapshot +async def test_outlet_insertion_ok( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + local_oauth_impl: ClientSession, + mock_get_devices_two_outlets, + mock_get_status_filled, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, +) -> None: + """Test iotty switch insertion.""" + + mock_config_entry.add_to_hass(hass) + + config_entry_oauth2_flow.async_register_implementation( + hass, DOMAIN, local_oauth_impl + ) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + + # Should have two devices + assert hass.states.async_entity_ids_count() == 2 + assert hass.states.async_entity_ids() == snapshot + + mock_get_devices_two_outlets.return_value = test_ou_one_added + + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Should have three devices + assert hass.states.async_entity_ids_count() == 3 + assert hass.states.async_entity_ids() == snapshot + + async def test_api_not_ok_entities_stay_the_same_as_before( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index ab5998c922f..031ff3c31d4 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -6,6 +6,7 @@ from pyipma.forecast import Forecast, Forecast_Location, Weather_Type from pyipma.observation import Observation from pyipma.rcm import RCM from pyipma.uv import UV +from pyipma.warnings import Warning from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME @@ -20,6 +21,20 @@ ENTRY_CONFIG = { class MockLocation: """Mock Location from pyipma.""" + async def warnings(self, api): + """Mock Warnings.""" + return [ + Warning( + text="Na costa Sul, ondas de sueste com 2 a 2,5 metros, em especial " + "no barlavento.", + awarenessTypeName="Agitação Marítima", + idAreaAviso="FAR", + startTime=datetime(2024, 12, 26, 12, 24), + awarenessLevelID="yellow", + endTime=datetime(2024, 12, 28, 6, 0), + ) + ] + async def fire_risk(self, api): """Mock Fire Risk.""" return RCM("some place", 3, (0, 0)) diff --git a/tests/components/ipma/test_sensor.py b/tests/components/ipma/test_sensor.py index adff8206add..455a85002d3 100644 --- a/tests/components/ipma/test_sensor.py +++ b/tests/components/ipma/test_sensor.py @@ -35,3 +35,19 @@ async def test_ipma_uv_index_create_sensors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.hometown_uv_index") assert state.state == "6" + + +async def test_ipma_warning_create_sensors(hass: HomeAssistant) -> None: + """Test creation of warning sensors.""" + + with patch("pyipma.location.Location.get", return_value=MockLocation()): + entry = MockConfigEntry(domain="ipma", data=ENTRY_CONFIG) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hometown_weather_alert") + + assert state.state == "yellow" + + assert state.attributes["awarenessTypeName"] == "Agitação Marítima" diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index ca374bd7e5e..89b54e22bbb 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -2,9 +2,9 @@ from ipaddress import ip_address -from homeassistant.components import zeroconf from homeassistant.components.ipp.const import CONF_BASE_PATH from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo ATTR_HOSTNAME = "hostname" ATTR_PROPERTIES = "properties" @@ -30,7 +30,7 @@ MOCK_USER_INPUT = { CONF_BASE_PATH: BASE_PATH, } -MOCK_ZEROCONF_IPP_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_IPP_SERVICE_INFO = ZeroconfServiceInfo( type=IPP_ZEROCONF_SERVICE_TYPE, name=f"{ZEROCONF_NAME}.{IPP_ZEROCONF_SERVICE_TYPE}", ip_address=ip_address(ZEROCONF_HOST), @@ -40,7 +40,7 @@ MOCK_ZEROCONF_IPP_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( properties={"rp": ZEROCONF_RP}, ) -MOCK_ZEROCONF_IPPS_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_IPPS_SERVICE_INFO = ZeroconfServiceInfo( type=IPPS_ZEROCONF_SERVICE_TYPE, name=f"{ZEROCONF_NAME}.{IPPS_ZEROCONF_SERVICE_TYPE}", ip_address=ip_address(ZEROCONF_HOST), diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index 3f910399ad8..f8e0578a6b9 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -73,6 +74,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -126,6 +128,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -179,6 +182,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -232,6 +236,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -283,6 +288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -332,6 +338,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py index 9091694e6a5..63c7d129987 100644 --- a/tests/components/iron_os/conftest.py +++ b/tests/components/iron_os/conftest.py @@ -6,16 +6,25 @@ from unittest.mock import AsyncMock, MagicMock, patch from bleak.backends.device import BLEDevice from habluetooth import BluetoothServiceInfoBleak from pynecil import ( + AnimationSpeed, + AutostartMode, + BatteryType, DeviceInfoResponse, LatestRelease, LiveDataResponse, + LockingMode, + LogoDuration, OperatingMode, PowerSource, + ScreenOrientationMode, + ScrollSpeed, SettingsDataResponse, + TempUnit, ) import pytest from homeassistant.components.iron_os import DOMAIN +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import CONF_ADDRESS from tests.common import MockConfigEntry @@ -102,6 +111,19 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture(name="config_entry_ignored") +def mock_config_entry_ignored() -> MockConfigEntry: + """Mock Pinecil configuration entry for ignored device.""" + return MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_NAME, + data={}, + unique_id="c0:ff:ee:c0:ff:ee", + entry_id="1234567890", + source=SOURCE_IGNORE, + ) + + @pytest.fixture(name="ble_device") def mock_ble_device() -> Generator[MagicMock]: """Mock BLEDevice.""" @@ -151,7 +173,7 @@ def mock_pynecil() -> Generator[AsyncMock]: client.get_settings.return_value = SettingsDataResponse( sleep_temp=150, sleep_timeout=5, - min_dc_voltage_cells=0, + min_dc_voltage_cells=BatteryType.BATTERY_3S, min_volltage_per_cell=3.3, qc_ideal_voltage=9.0, accel_sensitivity=7, @@ -168,6 +190,21 @@ def mock_pynecil() -> Generator[AsyncMock]: hall_sensitivity=7, pd_negotiation_timeout=2.0, display_brightness=3, + orientation_mode=ScreenOrientationMode.RIGHT_HANDED, + animation_speed=AnimationSpeed.MEDIUM, + autostart_mode=AutostartMode.IDLE, + temp_unit=TempUnit.CELSIUS, + desc_scroll_speed=ScrollSpeed.FAST, + logo_duration=LogoDuration.LOOP, + locking_mode=LockingMode.FULL_LOCKING, + animation_loop=True, + cooling_temp_blink=True, + idle_screen_details=True, + solder_screen_details=True, + invert_buttons=True, + display_invert=True, + calibrate_cjc=True, + usb_pd_mode=True, ) client.get_live_data.return_value = LiveDataResponse( live_temp=298, diff --git a/tests/components/iron_os/snapshots/test_binary_sensor.ambr b/tests/components/iron_os/snapshots/test_binary_sensor.ambr index 17b49c1d687..c36c1cc42ff 100644 --- a/tests/components/iron_os/snapshots/test_binary_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/iron_os/snapshots/test_button.ambr b/tests/components/iron_os/snapshots/test_button.ambr new file mode 100644 index 00000000000..c9ff9181515 --- /dev/null +++ b/tests/components/iron_os/snapshots/test_button.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_button_platform[button.pinecil_restore_default_settings-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pinecil_restore_default_settings', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Restore default settings', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_settings_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_platform[button.pinecil_restore_default_settings-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Restore default settings', + }), + 'context': , + 'entity_id': 'button.pinecil_restore_default_settings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button_platform[button.pinecil_save_settings-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.pinecil_save_settings', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Save settings', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_settings_save', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_platform[button.pinecil_save_settings-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Save settings', + }), + 'context': , + 'entity_id': 'button.pinecil_save_settings', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/iron_os/snapshots/test_diagnostics.ambr b/tests/components/iron_os/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..f8db1262254 --- /dev/null +++ b/tests/components/iron_os/snapshots/test_diagnostics.ambr @@ -0,0 +1,18 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'config_entry_data': dict({ + 'address': 'c0:ff:ee:c0:ff:ee', + }), + 'device_info': dict({ + '__type': "", + 'repr': "DeviceInfoResponse(build='v2.22', device_id='c0ffeeC0', address='c0:ff:ee:c0:ff:ee', device_sn='0000c0ffeec0ffee', name='Pinecil-C0FFEEE', is_synced=False)", + }), + 'live_data': dict({ + '__type': "", + 'repr': 'LiveDataResponse(live_temp=298, setpoint_temp=300, dc_voltage=20.6, handle_temp=36.3, pwm_level=41, power_src=, tip_resistance=6.2, uptime=1671, movement_time=10000, max_tip_temp_ability=460, tip_voltage=2212, hall_sensor=0, operating_mode=, estimated_power=24.8)', + }), + 'settings_data': dict({ + }), + }) +# --- diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index 24663cc4b0f..62fcd120201 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 10, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -68,6 +69,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -124,6 +126,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -179,6 +182,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -234,6 +238,7 @@ 'step': 2.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -290,6 +295,7 @@ 'step': 250, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -346,6 +352,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -402,6 +409,7 @@ 'step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -458,6 +466,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -514,6 +523,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -569,6 +579,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -620,12 +631,13 @@ }), 'area_id': None, 'capabilities': dict({ - 'max': 12, + 'max': 120, 'min': 0, 'mode': , - 'step': 0.1, + 'step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -656,10 +668,10 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Pinecil Power limit', - 'max': 12, + 'max': 120, 'min': 0, 'mode': , - 'step': 0.1, + 'step': 5, 'unit_of_measurement': , }), 'context': , @@ -682,6 +694,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -739,6 +752,7 @@ 'step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -796,6 +810,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -852,6 +867,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -909,6 +925,7 @@ 'step': 10, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -966,6 +983,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1022,6 +1040,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/iron_os/snapshots/test_select.ambr b/tests/components/iron_os/snapshots/test_select.ambr new file mode 100644 index 00000000000..10aacc838df --- /dev/null +++ b/tests/components/iron_os/snapshots/test_select.ambr @@ -0,0 +1,533 @@ +# serializer version: 1 +# name: test_state[select.pinecil_animation_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'slow', + 'medium', + 'fast', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_animation_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Animation speed', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_animation_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_animation_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Animation speed', + 'options': list([ + 'off', + 'slow', + 'medium', + 'fast', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_animation_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'medium', + }) +# --- +# name: test_state[select.pinecil_boot_logo_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'seconds_1', + 'seconds_2', + 'seconds_3', + 'seconds_4', + 'seconds_5', + 'loop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_boot_logo_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Boot logo duration', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_logo_duration', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_boot_logo_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Boot logo duration', + 'options': list([ + 'off', + 'seconds_1', + 'seconds_2', + 'seconds_3', + 'seconds_4', + 'seconds_5', + 'loop', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_boot_logo_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'loop', + }) +# --- +# name: test_state[select.pinecil_button_locking_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'boost_only', + 'full_locking', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_button_locking_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Button locking mode', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_locking_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_button_locking_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Button locking mode', + 'options': list([ + 'off', + 'boost_only', + 'full_locking', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_button_locking_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'full_locking', + }) +# --- +# name: test_state[select.pinecil_display_orientation_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'right_handed', + 'left_handed', + 'auto', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_display_orientation_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Display orientation mode', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_orientation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_display_orientation_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Display orientation mode', + 'options': list([ + 'right_handed', + 'left_handed', + 'auto', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_display_orientation_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'right_handed', + }) +# --- +# name: test_state[select.pinecil_power_delivery_3_1_epr-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'on', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_power_delivery_3_1_epr', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power Delivery 3.1 EPR', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_usb_pd_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_power_delivery_3_1_epr-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Power Delivery 3.1 EPR', + 'options': list([ + 'off', + 'on', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_power_delivery_3_1_epr', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[select.pinecil_power_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'no_battery', + 'battery_3s', + 'battery_4s', + 'battery_5s', + 'battery_6s', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_power_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power source', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_min_dc_voltage_cells', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_power_source-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Power source', + 'options': list([ + 'no_battery', + 'battery_3s', + 'battery_4s', + 'battery_5s', + 'battery_6s', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_power_source', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'battery_3s', + }) +# --- +# name: test_state[select.pinecil_scrolling_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'slow', + 'fast', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_scrolling_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Scrolling speed', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_desc_scroll_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_scrolling_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Scrolling speed', + 'options': list([ + 'slow', + 'fast', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_scrolling_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'fast', + }) +# --- +# name: test_state[select.pinecil_start_up_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'disabled', + 'soldering', + 'sleeping', + 'idle', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_start_up_behavior', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start-up behavior', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_autostart_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_start_up_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Start-up behavior', + 'options': list([ + 'disabled', + 'soldering', + 'sleeping', + 'idle', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_start_up_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_state[select.pinecil_temperature_display_unit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'celsius', + 'fahrenheit', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.pinecil_temperature_display_unit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature display unit', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_unit', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[select.pinecil_temperature_display_unit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Temperature display unit', + 'options': list([ + 'celsius', + 'fahrenheit', + ]), + }), + 'context': , + 'entity_id': 'select.pinecil_temperature_display_unit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'celsius', + }) +# --- diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr index 9ab5d47eec8..6a30aa6632b 100644 --- a/tests/components/iron_os/snapshots/test_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -159,6 +162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -210,6 +214,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -259,6 +264,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -313,9 +319,19 @@ 'sleeping', 'settings', 'debug', + 'soldering_profile', + 'temperature_adjust', + 'usb_pd_debug', + 'thermal_runaway', + 'startup_logo', + 'cjc_calibration', + 'startup_warnings', + 'initialisation_done', + 'hibernating', ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -354,6 +370,15 @@ 'sleeping', 'settings', 'debug', + 'soldering_profile', + 'temperature_adjust', + 'usb_pd_debug', + 'thermal_runaway', + 'startup_logo', + 'cjc_calibration', + 'startup_warnings', + 'initialisation_done', + 'hibernating', ]), }), 'context': , @@ -373,6 +398,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -432,6 +458,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -487,6 +514,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -541,6 +569,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -591,6 +620,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -642,6 +672,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/iron_os/snapshots/test_switch.ambr b/tests/components/iron_os/snapshots/test_switch.ambr new file mode 100644 index 00000000000..a3d28e58d63 --- /dev/null +++ b/tests/components/iron_os/snapshots/test_switch.ambr @@ -0,0 +1,330 @@ +# serializer version: 1 +# name: test_switch_platform[switch.pinecil_animation_loop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_animation_loop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Animation loop', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_animation_loop', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_animation_loop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Animation loop', + }), + 'context': , + 'entity_id': 'switch.pinecil_animation_loop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_calibrate_cjc-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_calibrate_cjc', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Calibrate CJC', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_calibrate_cjc', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_calibrate_cjc-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Calibrate CJC', + }), + 'context': , + 'entity_id': 'switch.pinecil_calibrate_cjc', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_cool_down_screen_flashing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_cool_down_screen_flashing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cool down screen flashing', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_cooling_temp_blink', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_cool_down_screen_flashing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Cool down screen flashing', + }), + 'context': , + 'entity_id': 'switch.pinecil_cool_down_screen_flashing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_idle_screen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_detailed_idle_screen', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Detailed idle screen', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_idle_screen_details', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_idle_screen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Detailed idle screen', + }), + 'context': , + 'entity_id': 'switch.pinecil_detailed_idle_screen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_solder_screen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_detailed_solder_screen', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Detailed solder screen', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_solder_screen_details', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_detailed_solder_screen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Detailed solder screen', + }), + 'context': , + 'entity_id': 'switch.pinecil_detailed_solder_screen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_invert_screen-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_invert_screen', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Invert screen', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_display_invert', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_invert_screen-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Invert screen', + }), + 'context': , + 'entity_id': 'switch.pinecil_invert_screen', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_platform[switch.pinecil_swap_buttons-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.pinecil_swap_buttons', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Swap +/- buttons', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_invert_buttons', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_platform[switch.pinecil_swap_buttons-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Swap +/- buttons', + }), + 'context': , + 'entity_id': 'switch.pinecil_swap_buttons', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/iron_os/snapshots/test_update.ambr b/tests/components/iron_os/snapshots/test_update.ambr index e0872d032ec..f2db3246158 100644 --- a/tests/components/iron_os/snapshots/test_update.ambr +++ b/tests/components/iron_os/snapshots/test_update.ambr @@ -9,6 +9,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/iron_os/test_button.py b/tests/components/iron_os/test_button.py new file mode 100644 index 00000000000..ce8a3ca491e --- /dev/null +++ b/tests/components/iron_os/test_button.py @@ -0,0 +1,106 @@ +"""Tests for the IronOS button platform.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +from pynecil import CharSetting, CommunicationError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +async def button_only() -> AsyncGenerator[None]: + """Enable only the button platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.BUTTON], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_button_platform( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test the IronOS button platform.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "call_args"), + [ + ("button.pinecil_save_settings", (CharSetting.SETTINGS_SAVE, True)), + ("button.pinecil_restore_default_settings", (CharSetting.SETTINGS_RESET, True)), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_button_press( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + entity_id: str, + call_args: tuple[tuple[CharSetting, bool]], +) -> None: + """Test button press method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_pynecil.write.assert_called_once_with(*call_args) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_button_press_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test button press method.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_pynecil.write.side_effect = CommunicationError + + with pytest.raises( + ServiceValidationError, + match="Failed to submit setting to device, try again later", + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.pinecil_save_settings"}, + blocking=True, + ) diff --git a/tests/components/iron_os/test_config_flow.py b/tests/components/iron_os/test_config_flow.py index 231ec6cc3d6..88bef117c26 100644 --- a/tests/components/iron_os/test_config_flow.py +++ b/tests/components/iron_os/test_config_flow.py @@ -4,6 +4,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock +import pytest + from homeassistant.components.iron_os import DOMAIN from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER from homeassistant.core import HomeAssistant @@ -11,9 +13,12 @@ from homeassistant.data_entry_flow import FlowResultType from .conftest import DEFAULT_NAME, PINECIL_SERVICE_INFO, USER_INPUT +from tests.common import MockConfigEntry -async def test_form( - hass: HomeAssistant, mock_setup_entry: AsyncMock, discovery: MagicMock + +@pytest.mark.usefixtures("discovery") +async def test_async_step_user( + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test the user config flow.""" result = await hass.config_entries.flow.async_init( @@ -32,10 +37,31 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("discovery") +async def test_async_step_user_device_added_between_steps( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test the device gets added via another flow between steps.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_setup_entry") async def test_form_no_device_discovered( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - discovery: MagicMock, + hass: HomeAssistant, discovery: MagicMock ) -> None: """Test setup with no device discoveries.""" discovery.return_value = [] @@ -48,7 +74,7 @@ async def test_form_no_device_discovered( async def test_async_step_bluetooth(hass: HomeAssistant) -> None: - """Test discovery via bluetooth..""" + """Test discovery via bluetooth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, @@ -64,3 +90,44 @@ async def test_async_step_bluetooth(hass: HomeAssistant) -> None: assert result["title"] == DEFAULT_NAME assert result["data"] == {} assert result["result"].unique_id == "c0:ff:ee:c0:ff:ee" + + +async def test_async_step_bluetooth_devices_already_setup( + hass: HomeAssistant, config_entry: AsyncMock +) -> None: + """Test we can't start a flow if there is already a config entry.""" + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=PINECIL_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("discovery") +async def test_async_step_user_setup_replaces_igonored_device( + hass: HomeAssistant, config_entry_ignored: AsyncMock +) -> None: + """Test the user initiated form can replace an ignored device.""" + + config_entry_ignored.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == {} + assert result["result"].unique_id == "c0:ff:ee:c0:ff:ee" diff --git a/tests/components/iron_os/test_diagnostics.py b/tests/components/iron_os/test_diagnostics.py new file mode 100644 index 00000000000..05f627e6bc6 --- /dev/null +++ b/tests/components/iron_os/test_diagnostics.py @@ -0,0 +1,29 @@ +"""Tests for IronOS diagnostics.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) diff --git a/tests/components/iron_os/test_init.py b/tests/components/iron_os/test_init.py index 21194a55eea..4749e1b6199 100644 --- a/tests/components/iron_os/test_init.py +++ b/tests/components/iron_os/test_init.py @@ -1,6 +1,6 @@ """Test init of IronOS integration.""" -from datetime import datetime, timedelta +from datetime import timedelta from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory @@ -61,7 +61,7 @@ async def test_setup_config_entry_not_ready( config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - freezer.tick(timedelta(seconds=60)) + freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -73,6 +73,7 @@ async def test_settings_exception( hass: HomeAssistant, config_entry: MockConfigEntry, mock_pynecil: AsyncMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test skipping of settings on exception.""" mock_pynecil.get_settings.side_effect = CommunicationError @@ -80,7 +81,8 @@ async def test_settings_exception( config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - async_fire_time_changed(hass, datetime.now() + timedelta(seconds=60)) + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/iron_os/test_number.py b/tests/components/iron_os/test_number.py index e0617a5012f..bdec922a88c 100644 --- a/tests/components/iron_os/test_number.py +++ b/tests/components/iron_os/test_number.py @@ -50,7 +50,7 @@ async def test_state( assert config_entry.state is ConfigEntryState.LOADED - freezer.tick(timedelta(seconds=60)) + freezer.tick(timedelta(seconds=3)) async_fire_time_changed(hass) await hass.async_block_till_done() await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) @@ -126,7 +126,7 @@ async def test_state( 2.0, 2.0, ), - ("number.pinecil_power_limit", CharSetting.POWER_LIMIT, 12.0, 12.0), + ("number.pinecil_power_limit", CharSetting.POWER_LIMIT, 120, 120), ("number.pinecil_quick_charge_voltage", CharSetting.QC_IDEAL_VOLTAGE, 9.0, 9.0), ( "number.pinecil_short_press_temperature_step", diff --git a/tests/components/iron_os/test_select.py b/tests/components/iron_os/test_select.py new file mode 100644 index 00000000000..8cc848dd4cb --- /dev/null +++ b/tests/components/iron_os/test_select.py @@ -0,0 +1,170 @@ +"""Tests for the IronOS select platform.""" + +from collections.abc import AsyncGenerator +from datetime import timedelta +from enum import Enum +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pynecil import ( + AnimationSpeed, + BatteryType, + CharSetting, + CommunicationError, + LockingMode, + LogoDuration, + ScreenOrientationMode, + ScrollSpeed, + TempUnit, + USBPDMode, +) +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(autouse=True) +async def select_only() -> AsyncGenerator[None]: + """Enable only the select platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.SELECT], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_state( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the IronOS select platform states.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "option", "call_params"), + [ + ( + "select.pinecil_power_source", + "battery_3s", + (CharSetting.MIN_DC_VOLTAGE_CELLS, BatteryType.BATTERY_3S), + ), + ( + "select.pinecil_display_orientation_mode", + "right_handed", + (CharSetting.ORIENTATION_MODE, ScreenOrientationMode.RIGHT_HANDED), + ), + ( + "select.pinecil_animation_speed", + "medium", + (CharSetting.ANIMATION_SPEED, AnimationSpeed.MEDIUM), + ), + ( + "select.pinecil_temperature_display_unit", + "fahrenheit", + (CharSetting.TEMP_UNIT, TempUnit.FAHRENHEIT), + ), + ( + "select.pinecil_scrolling_speed", + "fast", + (CharSetting.DESC_SCROLL_SPEED, ScrollSpeed.FAST), + ), + ( + "select.pinecil_button_locking_mode", + "full_locking", + (CharSetting.LOCKING_MODE, LockingMode.FULL_LOCKING), + ), + ( + "select.pinecil_boot_logo_duration", + "loop", + (CharSetting.LOGO_DURATION, LogoDuration.LOOP), + ), + ( + "select.pinecil_power_delivery_3_1_epr", + "on", + (CharSetting.USB_PD_MODE, USBPDMode.ON), + ), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_select_option( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + entity_id: str, + option: str, + call_params: tuple[Enum, ...], +) -> None: + """Test the IronOS select option service.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + service_data={ATTR_OPTION: option}, + target={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(mock_pynecil.write.mock_calls) == 1 + mock_pynecil.write.assert_called_once_with(*call_params) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_select_option_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test the IronOS select option service exception.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_pynecil.write.side_effect = CommunicationError + + with pytest.raises( + ServiceValidationError, + match="Failed to submit setting to device, try again later", + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + service_data={ATTR_OPTION: "battery_3s"}, + target={ATTR_ENTITY_ID: "select.pinecil_power_source"}, + blocking=True, + ) diff --git a/tests/components/iron_os/test_switch.py b/tests/components/iron_os/test_switch.py new file mode 100644 index 00000000000..d52c3fd333b --- /dev/null +++ b/tests/components/iron_os/test_switch.py @@ -0,0 +1,145 @@ +"""Tests for the IronOS switch platform.""" + +from collections.abc import AsyncGenerator +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pynecil import CharSetting, CommunicationError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(autouse=True) +async def switch_only() -> AsyncGenerator[None]: + """Enable only the switch platform.""" + with patch( + "homeassistant.components.iron_os.PLATFORMS", + [Platform.SWITCH], + ): + yield + + +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "mock_pynecil", "ble_device" +) +async def test_switch_platform( + hass: HomeAssistant, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the IronOS switch platform.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "target"), + [ + ("switch.pinecil_animation_loop", CharSetting.ANIMATION_LOOP), + ("switch.pinecil_calibrate_cjc", CharSetting.CALIBRATE_CJC), + ("switch.pinecil_cool_down_screen_flashing", CharSetting.COOLING_TEMP_BLINK), + ("switch.pinecil_detailed_idle_screen", CharSetting.IDLE_SCREEN_DETAILS), + ("switch.pinecil_detailed_solder_screen", CharSetting.SOLDER_SCREEN_DETAILS), + ("switch.pinecil_invert_screen", CharSetting.DISPLAY_INVERT), + ("switch.pinecil_swap_buttons", CharSetting.INVERT_BUTTONS), + ], +) +@pytest.mark.parametrize( + ("service", "value"), + [ + (SERVICE_TOGGLE, False), + (SERVICE_TURN_OFF, False), + (SERVICE_TURN_ON, True), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_turn_on_off_toggle( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + freezer: FrozenDateTimeFactory, + service: str, + value: bool, + entity_id: str, + target: CharSetting, +) -> None: + """Test the IronOS switch turn on/off, toggle services.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + freezer.tick(timedelta(seconds=3)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(mock_pynecil.write.mock_calls) == 1 + mock_pynecil.write.assert_called_once_with(target, value) + + +@pytest.mark.parametrize( + "service", + [SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON], +) +@pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "ble_device", "mock_pynecil" +) +async def test_turn_on_off_toggle_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, + service: str, +) -> None: + """Test the IronOS switch turn on/off, toggle service exceptions.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_pynecil.write.side_effect = CommunicationError + + with pytest.raises( + ServiceValidationError, + match="Failed to submit setting to device, try again later", + ): + await hass.services.async_call( + SWITCH_DOMAIN, + service, + service_data={ATTR_ENTITY_ID: "switch.pinecil_animation_loop"}, + blocking=True, + ) diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 522006b0847..90a3a90c451 100644 --- a/tests/components/islamic_prayer_times/__init__.py +++ b/tests/components/islamic_prayer_times/__init__.py @@ -3,7 +3,7 @@ from datetime import datetime from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util MOCK_USER_INPUT = { CONF_NAME: "Home", diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 7961b79676b..5ae11d8f850 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import NOW, PRAYER_TIMES diff --git a/tests/components/israel_rail/snapshots/test_sensor.ambr b/tests/components/israel_rail/snapshots/test_sensor.ambr index f851f1cd726..610c2c53e22 100644 --- a/tests/components/israel_rail/snapshots/test_sensor.ambr +++ b/tests/components/israel_rail/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -197,6 +201,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -244,6 +249,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ista_ecotrend/snapshots/test_init.ambr b/tests/components/ista_ecotrend/snapshots/test_init.ambr index c84d55c059c..7329eec7f70 100644 --- a/tests/components/ista_ecotrend/snapshots/test_init.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://ecotrend.ista.de/', 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://ecotrend.ista.de/', 'connections': set({ }), diff --git a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr index b5056019c74..296ce26c7f2 100644 --- a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -61,6 +62,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -115,6 +117,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -169,6 +172,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -223,6 +227,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -277,6 +282,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -331,6 +337,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -385,6 +392,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -439,6 +447,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -492,6 +501,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -546,6 +556,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -600,6 +611,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -654,6 +666,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -708,6 +721,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -762,6 +776,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -816,6 +831,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ista_ecotrend/test_statistics.py b/tests/components/ista_ecotrend/test_statistics.py index 21877f686df..aa4f71037c4 100644 --- a/tests/components/ista_ecotrend/test_statistics.py +++ b/tests/components/ista_ecotrend/test_statistics.py @@ -41,7 +41,7 @@ async def test_statistics_import( # Test that consumption statistics for 2 months have been added for entity in entities: - statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix("sensor.")}" + statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix('sensor.')}" stats = await hass.async_add_executor_job( statistics_during_period, hass, @@ -70,7 +70,7 @@ async def test_statistics_import( await async_wait_recording_done(hass) for entity in entities: - statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix("sensor.")}" + statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix('sensor.')}" stats = await hass.async_add_executor_job( statistics_during_period, hass, diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 2bc1fff222f..3a688f450d0 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -7,7 +7,6 @@ from pyisy import ISYConnectionError, ISYInvalidAuthError import pytest from homeassistant import config_entries -from homeassistant.components import dhcp, ssdp from homeassistant.components.isy994.const import ( CONF_TLS_VER, DOMAIN, @@ -18,6 +17,12 @@ from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IGNORE, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -255,13 +260,13 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + ATTR_UPNP_FRIENDLY_NAME: "myisy", + ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", }, ), ) @@ -274,13 +279,13 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + ATTR_UPNP_FRIENDLY_NAME: "myisy", + ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", }, ), ) @@ -322,13 +327,13 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://3.3.3.3{ISY_URL_POSTFIX}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + ATTR_UPNP_FRIENDLY_NAME: "myisy", + ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", }, ), ) @@ -353,13 +358,13 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://3.3.3.3/{ISY_URL_POSTFIX}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + ATTR_UPNP_FRIENDLY_NAME: "myisy", + ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", }, ), ) @@ -386,13 +391,13 @@ async def test_form_ssdp_existing_entry_with_alternate_port( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + ATTR_UPNP_FRIENDLY_NAME: "myisy", + ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", }, ), ) @@ -417,13 +422,13 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"https://3.3.3.3/{ISY_URL_POSTFIX}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "myisy", - ssdp.ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", + ATTR_UPNP_FRIENDLY_NAME: "myisy", + ATTR_UPNP_UDN: f"{UDN_UUID_PREFIX}{MOCK_UUID}", }, ), ) @@ -440,7 +445,7 @@ async def test_form_dhcp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", hostname="isy994-ems", macaddress=MOCK_MAC, @@ -476,7 +481,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", hostname="polisy", macaddress=MOCK_POLISY_MAC, @@ -516,7 +521,7 @@ async def test_form_dhcp_with_eisy(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", hostname="eisy", macaddress=MOCK_MAC, @@ -564,7 +569,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", hostname="isy994-ems", macaddress=MOCK_MAC, @@ -594,7 +599,7 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", hostname="isy994-ems", macaddress=MOCK_MAC, @@ -620,7 +625,7 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", hostname="isy994-ems", macaddress=MOCK_MAC, diff --git a/tests/components/ituran/conftest.py b/tests/components/ituran/conftest.py index ef22c90591d..5093cc301a1 100644 --- a/tests/components/ituran/conftest.py +++ b/tests/components/ituran/conftest.py @@ -3,6 +3,7 @@ from collections.abc import Generator from datetime import datetime from unittest.mock import AsyncMock, PropertyMock, patch +from zoneinfo import ZoneInfo import pytest @@ -56,7 +57,10 @@ class MockVehicle: self.gps_coordinates = (25.0, -71.0) self.address = "Bermuda Triangle" self.heading = 150 - self.last_update = datetime(2024, 1, 1, 0, 0, 0) + self.last_update = datetime( + 2024, 1, 1, 0, 0, 0, tzinfo=ZoneInfo("Asia/Jerusalem") + ) + self.battery_voltage = 12.0 @pytest.fixture diff --git a/tests/components/ituran/snapshots/test_device_tracker.ambr b/tests/components/ituran/snapshots/test_device_tracker.ambr index 3b650f7927f..e73f0cfee24 100644 --- a/tests/components/ituran/snapshots/test_device_tracker.ambr +++ b/tests/components/ituran/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ituran/snapshots/test_init.ambr b/tests/components/ituran/snapshots/test_init.ambr index 1e64ef9e850..b97aef6027b 100644 --- a/tests/components/ituran/snapshots/test_init.ambr +++ b/tests/components/ituran/snapshots/test_init.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/ituran/snapshots/test_sensor.ambr b/tests/components/ituran/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..f96190fdbc2 --- /dev/null +++ b/tests/components/ituran/snapshots/test_sensor.ambr @@ -0,0 +1,303 @@ +# serializer version: 1 +# name: test_sensor[sensor.mock_model_address-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_model_address', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Address', + 'platform': 'ituran', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'address', + 'unique_id': '12345678-address', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.mock_model_address-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'mock model Address', + }), + 'context': , + 'entity_id': 'sensor.mock_model_address', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Bermuda Triangle', + }) +# --- +# name: test_sensor[sensor.mock_model_battery_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_model_battery_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery voltage', + 'platform': 'ituran', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_voltage', + 'unique_id': '12345678-battery_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.mock_model_battery_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'mock model Battery voltage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_model_battery_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.0', + }) +# --- +# name: test_sensor[sensor.mock_model_heading-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_model_heading', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heading', + 'platform': 'ituran', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heading', + 'unique_id': '12345678-heading', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensor[sensor.mock_model_heading-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'mock model Heading', + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.mock_model_heading', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '150', + }) +# --- +# name: test_sensor[sensor.mock_model_last_update_from_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_model_last_update_from_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last update from vehicle', + 'platform': 'ituran', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_update_from_vehicle', + 'unique_id': '12345678-last_update_from_vehicle', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.mock_model_last_update_from_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'mock model Last update from vehicle', + }), + 'context': , + 'entity_id': 'sensor.mock_model_last_update_from_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-12-31T22:00:00+00:00', + }) +# --- +# name: test_sensor[sensor.mock_model_mileage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_model_mileage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Mileage', + 'platform': 'ituran', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mileage', + 'unique_id': '12345678-mileage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.mock_model_mileage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'mock model Mileage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_model_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- +# name: test_sensor[sensor.mock_model_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_model_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'ituran', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12345678-speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.mock_model_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'mock model Speed', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_model_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- diff --git a/tests/components/ituran/test_device_tracker.py b/tests/components/ituran/test_device_tracker.py index 7bcb314cde7..4fe92154e91 100644 --- a/tests/components/ituran/test_device_tracker.py +++ b/tests/components/ituran/test_device_tracker.py @@ -1,13 +1,13 @@ """Test the Ituran device_tracker.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from pyituran.exceptions import IturanApiError from syrupy.assertion import SnapshotAssertion from homeassistant.components.ituran.const import UPDATE_INTERVAL -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -24,7 +24,8 @@ async def test_device_tracker( mock_config_entry: MockConfigEntry, ) -> None: """Test state of device_tracker.""" - await setup_integration(hass, mock_config_entry) + with patch("homeassistant.components.ituran.PLATFORMS", [Platform.DEVICE_TRACKER]): + await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/ituran/test_sensor.py b/tests/components/ituran/test_sensor.py new file mode 100644 index 00000000000..a057f59b81f --- /dev/null +++ b/tests/components/ituran/test_sensor.py @@ -0,0 +1,76 @@ +"""Test the Ituran device_tracker.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from pyituran.exceptions import IturanApiError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.ituran.const import UPDATE_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_ituran: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state of sensor.""" + with patch("homeassistant.components.ituran.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_availability( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_ituran: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test sensor is marked as unavailable when we can't reach the Ituran service.""" + entities = [ + "sensor.mock_model_address", + "sensor.mock_model_battery_voltage", + "sensor.mock_model_heading", + "sensor.mock_model_last_update_from_vehicle", + "sensor.mock_model_mileage", + "sensor.mock_model_speed", + ] + + await setup_integration(hass, mock_config_entry) + + for entity_id in entities: + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + + mock_ituran.get_vehicles.side_effect = IturanApiError + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + for entity_id in entities: + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + mock_ituran.get_vehicles.side_effect = None + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + for entity_id in entities: + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 440bffc2256..ba0a2b4835e 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -6,7 +6,7 @@ from datetime import datetime from freezegun import freeze_time as alter_time # noqa: F401 from homeassistant.components import jewish_calendar -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util _LatLng = namedtuple("_LatLng", ["lat", "lng"]) # noqa: PYI024 diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 8abaaecb77d..5cfaaedfc72 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.jewish_calendar.const import ( from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import alter_time, make_jerusalem_test_params, make_nyc_test_params diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 4897ef7749b..aac0f583b05 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.components.jewish_calendar.const import ( from homeassistant.const import CONF_LANGUAGE, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import alter_time, make_jerusalem_test_params, make_nyc_test_params diff --git a/tests/components/kaleidescape/__init__.py b/tests/components/kaleidescape/__init__.py index 8182cb73743..6700218f8d4 100644 --- a/tests/components/kaleidescape/__init__.py +++ b/tests/components/kaleidescape/__init__.py @@ -1,13 +1,16 @@ """Tests for Kaleidescape integration.""" -from homeassistant.components import ssdp -from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) MOCK_HOST = "127.0.0.1" MOCK_SERIAL = "123456" MOCK_NAME = "Theater" -MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( +MOCK_SSDP_DISCOVERY_INFO = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{MOCK_HOST}", diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py index 8ca91d00386..dc0c89e8ea6 100644 --- a/tests/components/keenetic_ndms2/__init__.py +++ b/tests/components/keenetic_ndms2/__init__.py @@ -1,6 +1,5 @@ """Tests for the Keenetic NDMS2 component.""" -from homeassistant.components import ssdp from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import ( CONF_HOST, @@ -9,6 +8,11 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) MOCK_NAME = "Keenetic Ultra 2030" MOCK_IP = "0.0.0.0" @@ -30,12 +34,12 @@ MOCK_OPTIONS = { const.CONF_INTERFACES: ["Home", "VPS0"], } -MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( +MOCK_SSDP_DISCOVERY_INFO = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_LOCATION, upnp={ - ssdp.ATTR_UPNP_UDN: "uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, + ATTR_UPNP_UDN: "uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, }, ) diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 18bacc3a32c..7ddcdf38ed6 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -8,11 +8,15 @@ from ndms2_client.client import InterfaceInfo, RouterInfo import pytest from homeassistant import config_entries -from homeassistant.components import keenetic_ndms2 as keenetic, ssdp +from homeassistant.components import keenetic_ndms2 as keenetic from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, +) from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS, MOCK_SSDP_DISCOVERY_INFO @@ -200,7 +204,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=keenetic.DOMAIN, source=config_entries.SOURCE_IGNORE, - unique_id=MOCK_SSDP_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN], + unique_id=MOCK_SSDP_DISCOVERY_INFO.upnp[ATTR_UPNP_UDN], ) entry.add_to_hass(hass) @@ -222,7 +226,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: domain=keenetic.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS, - unique_id=MOCK_SSDP_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN], + unique_id=MOCK_SSDP_DISCOVERY_INFO.upnp[ATTR_UPNP_UDN], ) entry.add_to_hass(hass) @@ -247,7 +251,7 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) discovery_info.upnp = {**discovery_info.upnp} - discovery_info.upnp.pop(ssdp.ATTR_UPNP_UDN) + discovery_info.upnp.pop(ATTR_UPNP_UDN) result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, @@ -264,7 +268,7 @@ async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) discovery_info.upnp = {**discovery_info.upnp} - discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Suspicious device" + discovery_info.upnp[ATTR_UPNP_FRIENDLY_NAME] = "Suspicious device" result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_SSDP}, diff --git a/tests/components/kira/test_remote.py b/tests/components/kira/test_remote.py index ff3b28617d3..ad443fa3154 100644 --- a/tests/components/kira/test_remote.py +++ b/tests/components/kira/test_remote.py @@ -5,6 +5,8 @@ from unittest.mock import MagicMock from homeassistant.components.kira import remote as kira from homeassistant.core import HomeAssistant +from tests.common import MockEntityPlatform + SERVICE_SEND_COMMAND = "send_command" TEST_CONFIG = {kira.DOMAIN: {"devices": [{"host": "127.0.0.1", "port": 17324}]}} @@ -28,6 +30,8 @@ def test_service_call(hass: HomeAssistant) -> None: kira.setup_platform(hass, TEST_CONFIG, add_entities, DISCOVERY_INFO) assert len(DEVICES) == 1 remote = DEVICES[0] + remote.hass = hass + remote.platform = MockEntityPlatform(hass) assert remote.name == "kira" diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py index fe0fc95a918..3bd46f18765 100644 --- a/tests/components/kira/test_sensor.py +++ b/tests/components/kira/test_sensor.py @@ -5,6 +5,8 @@ from unittest.mock import MagicMock, patch from homeassistant.components.kira import sensor as kira from homeassistant.core import HomeAssistant +from tests.common import MockEntityPlatform + TEST_CONFIG = {kira.DOMAIN: {"sensors": [{"host": "127.0.0.1", "port": 17324}]}} DISCOVERY_INFO = {"name": "kira", "device": "kira"} @@ -29,6 +31,8 @@ def test_kira_sensor_callback( kira.setup_platform(hass, TEST_CONFIG, add_entities, DISCOVERY_INFO) assert len(DEVICES) == 1 sensor = DEVICES[0] + sensor.hass = hass + sensor.platform = MockEntityPlatform(hass) assert sensor.name == "kira" diff --git a/tests/components/kitchen_sink/snapshots/test_sensor.ambr b/tests/components/kitchen_sink/snapshots/test_sensor.ambr index bbf88c84eca..7b433c40170 100644 --- a/tests/components/kitchen_sink/snapshots/test_sensor.ambr +++ b/tests/components/kitchen_sink/snapshots/test_sensor.ambr @@ -69,3 +69,84 @@ }), }) # --- +# name: test_states_with_subentry + set({ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Outlet 1 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.outlet_1_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Outlet 2 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.outlet_2_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1500', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Sensor test', + }), + 'context': , + 'entity_id': 'sensor.sensor_test', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Statistics issues Issue 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.statistics_issues_issue_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Statistics issues Issue 2', + 'state_class': , + 'unit_of_measurement': 'dogs', + }), + 'context': , + 'entity_id': 'sensor.statistics_issues_issue_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Statistics issues Issue 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.statistics_issues_issue_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), + }) +# --- diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index fe4311ad711..5535554017f 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -19,6 +19,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -49,6 +50,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -81,6 +83,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -129,6 +132,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -159,6 +163,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -191,6 +196,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/kitchen_sink/test_backup.py b/tests/components/kitchen_sink/test_backup.py index 6a738094ae6..7c693abcda8 100644 --- a/tests/components/kitchen_sink/test_backup.py +++ b/tests/components/kitchen_sink/test_backup.py @@ -2,7 +2,7 @@ from collections.abc import AsyncGenerator from io import StringIO -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -14,6 +14,7 @@ from homeassistant.components.backup import ( ) from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import instance_id from homeassistant.setup import async_setup_component from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -54,7 +55,10 @@ async def test_agents_info( assert response["success"] assert response["result"] == { - "agents": [{"agent_id": "backup.local"}, {"agent_id": "kitchen_sink.syncer"}], + "agents": [ + {"agent_id": "backup.local", "name": "local"}, + {"agent_id": "kitchen_sink.syncer", "name": "syncer"}, + ], } config_entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -65,7 +69,9 @@ async def test_agents_info( response = await client.receive_json() assert response["success"] - assert response["result"] == {"agents": [{"agent_id": "backup.local"}]} + assert response["result"] == { + "agents": [{"agent_id": "backup.local", "name": "local"}] + } await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -75,7 +81,10 @@ async def test_agents_info( assert response["success"] assert response["result"] == { - "agents": [{"agent_id": "backup.local"}, {"agent_id": "kitchen_sink.syncer"}], + "agents": [ + {"agent_id": "backup.local", "name": "local"}, + {"agent_id": "kitchen_sink.syncer", "name": "syncer"}, + ], } @@ -93,18 +102,17 @@ async def test_agents_list_backups( assert response["result"]["backups"] == [ { "addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}], - "agent_ids": ["kitchen_sink.syncer"], + "agents": {"kitchen_sink.syncer": {"protected": False, "size": 1234}}, "backup_id": "abc123", "database_included": False, "date": "1970-01-01T00:00:00Z", + "extra_metadata": {}, "failed_agent_ids": [], "folders": ["media", "share"], "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Kitchen sink syncer", - "protected": False, - "size": 1234, - "with_strategy_settings": False, + "with_automatic_settings": None, } ] @@ -137,6 +145,10 @@ async def test_agents_upload( backup_id=backup_id, database_included=True, date="1970-01-01T00:00:00.000Z", + extra_metadata={ + "instance_id": await instance_id.async_get(hass), + "with_automatic_settings": False, + }, folders=[Folder.MEDIA, Folder.SHARE], homeassistant_included=True, homeassistant_version="2024.12.0", @@ -172,18 +184,17 @@ async def test_agents_upload( assert len(backup_list) == 2 assert backup_list[1] == { "addons": [{"name": "Test", "slug": "test", "version": "1.0.0"}], - "agent_ids": ["kitchen_sink.syncer"], + "agents": {"kitchen_sink.syncer": {"protected": False, "size": 0.0}}, "backup_id": "test-backup", "database_included": True, "date": "1970-01-01T00:00:00.000Z", + "extra_metadata": {"instance_id": ANY, "with_automatic_settings": False}, "failed_agent_ids": [], "folders": ["media", "share"], "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "protected": False, - "size": 0.0, - "with_strategy_settings": False, + "with_automatic_settings": False, } diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index 5f163d1342e..1eea1c8036b 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -104,3 +104,85 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert config_entry.options == {"section_1": {"bool": True, "int": 15}} await hass.async_block_till_done() + + +@pytest.mark.usefixtures("no_platforms") +async def test_subentry_flow(hass: HomeAssistant) -> None: + """Test config flow options.""" + config_entry = MockConfigEntry(domain=DOMAIN) + 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.subentries.async_init( + (config_entry.entry_id, "entity"), + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "add_sensor" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={"name": "Sensor 1", "state": 15}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + subentry_id = list(config_entry.subentries)[0] + assert config_entry.subentries == { + subentry_id: config_entries.ConfigSubentry( + data={"state": 15}, + subentry_id=subentry_id, + subentry_type="entity", + title="Sensor 1", + unique_id=None, + ) + } + + await hass.async_block_till_done() + + +@pytest.mark.usefixtures("no_platforms") +async def test_subentry_reconfigure_flow(hass: HomeAssistant) -> None: + """Test config flow options.""" + subentry_id = "mock_id" + config_entry = MockConfigEntry( + domain=DOMAIN, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"state": 15}, + subentry_id="mock_id", + subentry_type="entity", + title="Sensor 1", + unique_id=None, + ) + ], + ) + 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 config_entry.start_subentry_reconfigure_flow( + hass, "entity", subentry_id + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_sensor" + + result = await hass.config_entries.subentries.async_configure( + result["flow_id"], + user_input={"name": "Renamed sensor 1", "state": 5}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + assert config_entry.subentries == { + subentry_id: config_entries.ConfigSubentry( + data={"state": 5}, + subentry_id=subentry_id, + subentry_type="entity", + title="Renamed sensor 1", + unique_id=None, + ) + } + + await hass.async_block_till_done() diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py index b832577a48a..7338c1dca99 100644 --- a/tests/components/kitchen_sink/test_init.py +++ b/tests/components/kitchen_sink/test_init.py @@ -17,7 +17,7 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from tests.components.recorder.common import async_wait_recording_done diff --git a/tests/components/kitchen_sink/test_sensor.py b/tests/components/kitchen_sink/test_sensor.py index c4b5f03499e..f980e39f652 100644 --- a/tests/components/kitchen_sink/test_sensor.py +++ b/tests/components/kitchen_sink/test_sensor.py @@ -5,11 +5,14 @@ from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant import config_entries from homeassistant.components.kitchen_sink import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + @pytest.fixture async def sensor_only() -> None: @@ -21,14 +24,41 @@ async def sensor_only() -> None: yield -@pytest.fixture(autouse=True) +@pytest.fixture async def setup_comp(hass: HomeAssistant, sensor_only): """Set up demo component.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() +@pytest.mark.usefixtures("setup_comp") async def test_states(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: """Test the expected sensor entities are added.""" states = hass.states.async_all() assert set(states) == snapshot + + +@pytest.mark.usefixtures("sensor_only") +async def test_states_with_subentry( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test the expected sensor entities are added.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"state": 15}, + subentry_id="blabla", + subentry_type="entity", + title="Sensor test", + unique_id=None, + ) + ], + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + states = hass.states.async_all() + assert set(states) == snapshot diff --git a/tests/components/knocki/snapshots/test_event.ambr b/tests/components/knocki/snapshots/test_event.ambr index fba1c90b45d..65fecd59739 100644 --- a/tests/components/knocki/snapshots/test_event.ambr +++ b/tests/components/knocki/snapshots/test_event.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/knx/README.md b/tests/components/knx/README.md index ef8398b3d17..71218010b45 100644 --- a/tests/components/knx/README.md +++ b/tests/components/knx/README.md @@ -3,17 +3,22 @@ A KNXTestKit instance can be requested from a fixture. It provides convenience methods to test outgoing KNX telegrams and inject incoming telegrams. To test something add a test function requesting the `hass` and `knx` fixture and -set up the KNX integration by passing a KNX config dict to `knx.setup_integration`. +set up the KNX integration with `knx.setup_integration`. +You can pass a KNX YAML-config dict or a ConfigStore fixture filename to the setup method. The fixture should be placed in the `tests/components/knx/fixtures` directory. ```python -async def test_something(hass, knx): - await knx.setup_integration({ +async def test_some_yaml(hass: HomeAssistant, knx: KNXTestKit): + await knx.setup_integration( + yaml_config={ "switch": { "name": "test_switch", "address": "1/2/3", } } ) + +async def test_some_config_store(hass: HomeAssistant, knx: KNXTestKit): + await knx.setup_integration(config_store_fixture="config_store_filename.json") ``` ## Asserting outgoing telegrams diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index c0ec1dd9b9a..c9092a1774f 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -44,7 +44,6 @@ from tests.common import MockConfigEntry, load_json_object_fixture from tests.typing import WebSocketGenerator FIXTURE_PROJECT_DATA = load_json_object_fixture("project.json", KNX_DOMAIN) -FIXTURE_CONFIG_STORAGE_DATA = load_json_object_fixture("config_store.json", KNX_DOMAIN) class KNXTestKit: @@ -52,10 +51,16 @@ class KNXTestKit: INDIVIDUAL_ADDRESS = "1.2.3" - def __init__(self, hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: + def __init__( + self, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], + ) -> None: """Init KNX test helper class.""" self.hass: HomeAssistant = hass self.mock_config_entry: MockConfigEntry = mock_config_entry + self.hass_storage: dict[str, Any] = hass_storage self.xknx: XKNX # outgoing telegrams will be put in the List instead of sent to the interface # telegrams to an InternalGroupAddress won't be queued here @@ -69,7 +74,10 @@ class KNXTestKit: assert test_state.attributes.get(attribute) == value async def setup_integration( - self, config: ConfigType, add_entry_to_hass: bool = True + self, + yaml_config: ConfigType | None = None, + config_store_fixture: str | None = None, + add_entry_to_hass: bool = True, ) -> None: """Create the KNX integration.""" @@ -101,15 +109,21 @@ class KNXTestKit: self.xknx = args[0] return DEFAULT + if config_store_fixture: + self.hass_storage[KNX_CONFIG_STORAGE_KEY] = load_json_object_fixture( + config_store_fixture, KNX_DOMAIN + ) + if add_entry_to_hass: self.mock_config_entry.add_to_hass(self.hass) + knx_config = {KNX_DOMAIN: yaml_config or {}} with patch( "xknx.xknx.knx_interface_factory", return_value=knx_ip_interface_mock(), side_effect=fish_xknx, ): - await async_setup_component(self.hass, KNX_DOMAIN, {KNX_DOMAIN: config}) + await async_setup_component(self.hass, KNX_DOMAIN, knx_config) await self.hass.async_block_till_done() ######################## @@ -174,12 +188,12 @@ class KNXTestKit: ) telegram = self._outgoing_telegrams.pop(0) - assert isinstance( - telegram.payload, apci_type - ), f"APCI type mismatch in {telegram} - Expected: {apci_type.__name__}" - assert ( - telegram.destination_address == _expected_ga - ), f"Group address mismatch in {telegram} - Expected: {group_address}" + assert isinstance(telegram.payload, apci_type), ( + f"APCI type mismatch in {telegram} - Expected: {apci_type.__name__}" + ) + assert telegram.destination_address == _expected_ga, ( + f"Group address mismatch in {telegram} - Expected: {group_address}" + ) if payload is not None: assert ( telegram.payload.value.value == payload # type: ignore[attr-defined] @@ -306,9 +320,13 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -async def knx(hass: HomeAssistant, mock_config_entry: MockConfigEntry): +async def knx( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + hass_storage: dict[str, Any], +): """Create a KNX TestKit instance.""" - knx_test_kit = KNXTestKit(hass, mock_config_entry) + knx_test_kit = KNXTestKit(hass, mock_config_entry, hass_storage) yield knx_test_kit await knx_test_kit.assert_no_telegram() @@ -322,12 +340,6 @@ def load_knxproj(hass_storage: dict[str, Any]) -> None: } -@pytest.fixture -def load_config_store(hass_storage: dict[str, Any]) -> None: - """Mock KNX config store data.""" - hass_storage[KNX_CONFIG_STORAGE_KEY] = FIXTURE_CONFIG_STORAGE_DATA - - @pytest.fixture async def create_ui_entity( hass: HomeAssistant, @@ -335,7 +347,7 @@ async def create_ui_entity( hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], ) -> KnxEntityGenerator: - """Return a helper to create a KNX entities via WS. + """Return a helper to create KNX entities via WS. The KNX integration must be set up before using the helper. """ diff --git a/tests/components/knx/fixtures/config_store_binarysensor.json b/tests/components/knx/fixtures/config_store_binarysensor.json new file mode 100644 index 00000000000..427867cff8c --- /dev/null +++ b/tests/components/knx/fixtures/config_store_binarysensor.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "minor_version": 1, + "key": "knx/config_store.json", + "data": { + "entities": { + "light": {}, + "binary_sensor": { + "knx_es_01JJP1XDQRXB0W6YYGXW6Y1X10": { + "entity": { + "name": "test", + "device_info": null, + "entity_category": null + }, + "knx": { + "ga_sensor": { + "state": "3/2/21", + "passive": [] + }, + "respond_to_read": false, + "sync_state": true + } + } + } + } + } +} diff --git a/tests/components/knx/fixtures/config_store.json b/tests/components/knx/fixtures/config_store_light_switch.json similarity index 100% rename from tests/components/knx/fixtures/config_store.json rename to tests/components/knx/fixtures/config_store_light_switch.json diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index dbb8d2ee832..b93b7e965df 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -1,10 +1,19 @@ """Test KNX binary sensor.""" from datetime import timedelta +from typing import Any from freezegun.api import FrozenDateTimeFactory +import pytest -from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE +from homeassistant.components.knx.const import ( + CONF_CONTEXT_TIMEOUT, + CONF_IGNORE_INTERNAL_STATE, + CONF_INVERT, + CONF_RESET_AFTER, + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, +) from homeassistant.components.knx.schema import BinarySensorSchema from homeassistant.const import ( CONF_ENTITY_CATEGORY, @@ -12,10 +21,12 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, EntityCategory, + Platform, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er +from . import KnxEntityGenerator from .conftest import KNXTestKit from tests.common import ( @@ -60,7 +71,7 @@ async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit) -> None: { CONF_NAME: "test_invert", CONF_STATE_ADDRESS: "2/2/2", - BinarySensorSchema.CONF_INVERT: True, + CONF_INVERT: True, }, ] } @@ -113,7 +124,7 @@ async def test_binary_sensor_ignore_internal_state( { CONF_NAME: "test_ignore", CONF_STATE_ADDRESS: "2/2/2", - BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE: True, + CONF_IGNORE_INTERNAL_STATE: True, CONF_SYNC_STATE: False, }, ] @@ -156,7 +167,7 @@ async def test_binary_sensor_counter( { CONF_NAME: "test", CONF_STATE_ADDRESS: "2/2/2", - BinarySensorSchema.CONF_CONTEXT_TIMEOUT: context_timeout, + CONF_CONTEXT_TIMEOUT: context_timeout, CONF_SYNC_STATE: False, }, ] @@ -220,7 +231,7 @@ async def test_binary_sensor_reset( { CONF_NAME: "test", CONF_STATE_ADDRESS: "2/2/2", - BinarySensorSchema.CONF_RESET_AFTER: 1, + CONF_RESET_AFTER: 1, CONF_SYNC_STATE: False, }, ] @@ -279,7 +290,7 @@ async def test_binary_sensor_restore_invert(hass: HomeAssistant, knx) -> None: { CONF_NAME: "test", CONF_STATE_ADDRESS: _ADDRESS, - BinarySensorSchema.CONF_INVERT: True, + CONF_INVERT: True, CONF_SYNC_STATE: False, }, ] @@ -295,3 +306,44 @@ async def test_binary_sensor_restore_invert(hass: HomeAssistant, knx) -> None: await knx.receive_write(_ADDRESS, True) state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF + + +@pytest.mark.parametrize( + ("knx_data"), + [ + { + "ga_sensor": {"state": "2/2/2"}, + "sync_state": True, + }, + { + "ga_sensor": {"state": "2/2/2"}, + "sync_state": True, + "invert": True, + }, + ], +) +async def test_binary_sensor_ui_create( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + knx_data: dict[str, Any], +) -> None: + """Test creating a binary sensor.""" + await knx.setup_integration() + await create_ui_entity( + platform=Platform.BINARY_SENSOR, + entity_data={"name": "test"}, + knx_data=knx_data, + ) + # created entity sends read-request to KNX bus + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", not knx_data.get("invert")) + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_ON + + +async def test_binary_sensor_ui_load(knx: KNXTestKit) -> None: + """Test loading a binary sensor from storage.""" + await knx.setup_integration(config_store_fixture="config_store_binarysensor.json") + await knx.assert_read("3/2/21", response=True, ignore_order=True) + knx.assert_state("binary_sensor.test", STATE_ON) diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 8fb348f1724..b5a90428ef2 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -850,3 +850,91 @@ async def test_climate_humidity(hass: HomeAssistant, knx: KNXTestKit) -> None: HVACMode.HEAT, current_humidity=45.6, ) + + +async def test_swing(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX climate swing.""" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_SWING_ADDRESS: "1/2/6", + ClimateSchema.CONF_SWING_STATE_ADDRESS: "1/2/7", + } + } + ) + + # read states state updater + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + + # StateUpdater initialize state + await knx.receive_response("1/2/5", RAW_FLOAT_22_0) + await knx.receive_response("1/2/3", RAW_FLOAT_21_0) + + # Query status + await knx.assert_read("1/2/7") + await knx.receive_response("1/2/7", True) + knx.assert_state( + "climate.test", + HVACMode.HEAT, + swing_mode="on", + swing_modes=["on", "off"], + ) + + # turn off + await hass.services.async_call( + "climate", + "set_swing_mode", + {"entity_id": "climate.test", "swing_mode": "off"}, + blocking=True, + ) + await knx.assert_write("1/2/6", False) + knx.assert_state("climate.test", HVACMode.HEAT, swing_mode="off") + + +async def test_horizontal_swing(hass: HomeAssistant, knx: KNXTestKit) -> None: + """Test KNX climate horizontal swing.""" + await knx.setup_integration( + { + ClimateSchema.PLATFORM: { + CONF_NAME: "test", + ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", + ClimateSchema.CONF_SWING_HORIZONTAL_ADDRESS: "1/2/6", + ClimateSchema.CONF_SWING_HORIZONTAL_STATE_ADDRESS: "1/2/7", + } + } + ) + + # read states state updater + await knx.assert_read("1/2/3") + await knx.assert_read("1/2/5") + + # StateUpdater initialize state + await knx.receive_response("1/2/5", RAW_FLOAT_22_0) + await knx.receive_response("1/2/3", RAW_FLOAT_21_0) + + # Query status + await knx.assert_read("1/2/7") + await knx.receive_response("1/2/7", True) + knx.assert_state( + "climate.test", + HVACMode.HEAT, + swing_horizontal_mode="on", + swing_horizontal_modes=["on", "off"], + ) + + # turn off + await hass.services.async_call( + "climate", + "set_swing_horizontal_mode", + {"entity_id": "climate.test", "swing_horizontal_mode": "off"}, + blocking=True, + ) + await knx.assert_write("1/2/6", False) + knx.assert_state("climate.test", HVACMode.HEAT, swing_horizontal_mode="off") diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 2187721a518..3e4c9408542 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -7,6 +7,7 @@ import pytest from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io.gateway_scanner import GatewayDescriptor +from xknx.knxip.dib import TunnelingSlotStatus from xknx.secure.keyring import sync_load_keyring from xknx.telegram import IndividualAddress @@ -105,6 +106,7 @@ def _gateway_descriptor( port: int, supports_tunnelling_tcp: bool = False, requires_secure: bool = False, + slots: bool = True, ) -> GatewayDescriptor: """Get mock gw descriptor.""" descriptor = GatewayDescriptor( @@ -120,6 +122,12 @@ def _gateway_descriptor( ) descriptor.tunnelling_requires_secure = requires_secure descriptor.routing_requires_secure = requires_secure + if supports_tunnelling_tcp and slots: + descriptor.tunnelling_slots = { + IndividualAddress("1.0.240"): TunnelingSlotStatus(True, True, True), + IndividualAddress("1.0.241"): TunnelingSlotStatus(True, True, False), + IndividualAddress("1.0.242"): TunnelingSlotStatus(True, True, True), + } return descriptor @@ -791,12 +799,14 @@ async def test_tunneling_setup_for_multiple_found_gateways( hass: HomeAssistant, knx_setup ) -> None: """Test tunneling if multiple gateways are found.""" - gateway = _gateway_descriptor("192.168.0.1", 3675) - gateway2 = _gateway_descriptor("192.168.1.100", 3675) + gateway_udp = _gateway_descriptor("192.168.0.1", 3675) + gateway_tcp = _gateway_descriptor("192.168.1.100", 3675, True) with patch( "homeassistant.components.knx.config_flow.GatewayScanner" ) as gateway_scanner_mock: - gateway_scanner_mock.return_value = GatewayScannerMock([gateway, gateway2]) + gateway_scanner_mock.return_value = GatewayScannerMock( + [gateway_udp, gateway_tcp] + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -815,7 +825,7 @@ async def test_tunneling_setup_for_multiple_found_gateways( result = await hass.config_entries.flow.async_configure( tunnel_flow["flow_id"], - {CONF_KNX_GATEWAY: str(gateway)}, + {CONF_KNX_GATEWAY: str(gateway_udp)}, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { @@ -833,6 +843,110 @@ async def test_tunneling_setup_for_multiple_found_gateways( knx_setup.assert_called_once() +async def test_tunneling_setup_tcp_endpoint_select_skip( + hass: HomeAssistant, knx_setup +) -> None: + """Test tunneling TCP endpoint selection skipped if no slot info found.""" + gateway_udp = _gateway_descriptor("192.168.0.1", 3675) + gateway_tcp_no_slots = _gateway_descriptor("192.168.1.100", 3675, True, slots=False) + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock( + [gateway_udp, gateway_tcp_no_slots] + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + tunnel_flow = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + assert tunnel_flow["type"] is FlowResultType.FORM + assert tunnel_flow["step_id"] == "tunnel" + assert not tunnel_flow["errors"] + + result = await hass.config_entries.flow.async_configure( + tunnel_flow["flow_id"], + {CONF_KNX_GATEWAY: str(gateway_tcp_no_slots)}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.1.100", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_TUNNEL_ENDPOINT_IA: None, + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, + CONF_KNX_SECURE_USER_ID: None, + CONF_KNX_SECURE_USER_PASSWORD: None, + } + knx_setup.assert_called_once() + + +async def test_tunneling_setup_tcp_endpoint_select( + hass: HomeAssistant, knx_setup +) -> None: + """Test tunneling TCP endpoint selection.""" + gateway_tcp = _gateway_descriptor("192.168.1.100", 3675, True) + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway_tcp]) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + tunnel_flow = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + assert tunnel_flow["type"] is FlowResultType.FORM + assert tunnel_flow["step_id"] == "tunnel" + assert not tunnel_flow["errors"] + + endpoint_flow = await hass.config_entries.flow.async_configure( + tunnel_flow["flow_id"], + {CONF_KNX_GATEWAY: str(gateway_tcp)}, + ) + + assert endpoint_flow["type"] is FlowResultType.FORM + assert endpoint_flow["step_id"] == "tcp_tunnel_endpoint" + assert not endpoint_flow["errors"] + + result = await hass.config_entries.flow.async_configure( + endpoint_flow["flow_id"], + {CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.242"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "1.0.242 @ 1.0.0 - Test @ 192.168.1.100:3675" + assert result["data"] == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, + CONF_HOST: "192.168.1.100", + CONF_PORT: 3675, + CONF_KNX_INDIVIDUAL_ADDRESS: "0.0.240", + CONF_KNX_ROUTE_BACK: False, + CONF_KNX_TUNNEL_ENDPOINT_IA: "1.0.242", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, + CONF_KNX_SECURE_USER_ID: None, + CONF_KNX_SECURE_USER_PASSWORD: None, + } + knx_setup.assert_called_once() + + @pytest.mark.parametrize( "gateway", [ @@ -1164,7 +1278,7 @@ async def test_options_flow_connection_type( # usage of the already running XKNX instance for gateway scanner gateway = _gateway_descriptor("192.168.0.1", 3675) - await knx.setup_integration({}) + await knx.setup_integration() menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) with patch( @@ -1319,6 +1433,64 @@ async def test_options_flow_secure_manual_to_keyfile( knx_setup.assert_called_once() +async def test_options_flow_routing(hass: HomeAssistant, knx_setup) -> None: + """Test options flow changing routing settings.""" + mock_config_entry = MockConfigEntry( + title="KNX", + domain="knx", + data={ + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + gateway = _gateway_descriptor("192.168.0.1", 3676) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + with patch( + "homeassistant.components.knx.config_flow.GatewayScanner" + ) as gateway_scanner_mock: + gateway_scanner_mock.return_value = GatewayScannerMock([gateway]) + result = await hass.config_entries.options.async_configure( + menu_step["flow_id"], + {"next_step_id": "connection_type"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "connection_type" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "routing" + assert result2["errors"] == {} + + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + { + CONF_KNX_INDIVIDUAL_ADDRESS: "2.0.4", + }, + ) + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert mock_config_entry.data == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_LOCAL_IP: None, + CONF_KNX_INDIVIDUAL_ADDRESS: "2.0.4", + CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None, + CONF_KNX_SECURE_USER_ID: None, + CONF_KNX_SECURE_USER_PASSWORD: None, + CONF_KNX_TUNNEL_ENDPOINT_IA: None, + } + knx_setup.assert_called_once() + + async def test_options_communication_settings( hass: HomeAssistant, knx_setup, mock_config_entry: MockConfigEntry ) -> None: diff --git a/tests/components/knx/test_config_store.py b/tests/components/knx/test_config_store.py index 116f4b5d839..aee0a4036ff 100644 --- a/tests/components/knx/test_config_store.py +++ b/tests/components/knx/test_config_store.py @@ -25,7 +25,7 @@ async def test_create_entity( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity creation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_name = "Test no device" @@ -69,7 +69,7 @@ async def test_create_entity_error( hass_ws_client: WebSocketGenerator, ) -> None: """Test unsuccessful entity creation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) # create entity with invalid platform @@ -116,7 +116,7 @@ async def test_update_entity( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity update.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -163,7 +163,7 @@ async def test_update_entity_error( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity update.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -238,7 +238,7 @@ async def test_delete_entity( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity deletion.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -270,7 +270,7 @@ async def test_delete_entity_error( hass_storage: dict[str, Any], ) -> None: """Test unsuccessful entity deletion.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) # delete unknown entity @@ -307,7 +307,7 @@ async def test_get_entity_config( create_ui_entity: KnxEntityGenerator, ) -> None: """Test entity config retrieval.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) test_entity = await create_ui_entity( @@ -355,7 +355,7 @@ async def test_get_entity_config_error( error_message_start: str, ) -> None: """Test entity config retrieval errors.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id( @@ -376,7 +376,7 @@ async def test_validate_entity( hass_ws_client: WebSocketGenerator, ) -> None: """Test entity validation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id( diff --git a/tests/components/knx/test_device.py b/tests/components/knx/test_device.py index 04ff02f0611..356640dd8d0 100644 --- a/tests/components/knx/test_device.py +++ b/tests/components/knx/test_device.py @@ -22,7 +22,7 @@ async def test_create_device( hass_ws_client: WebSocketGenerator, ) -> None: """Test device creation.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id( @@ -50,12 +50,11 @@ async def test_remove_device( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, hass_ws_client: WebSocketGenerator, - load_config_store: None, hass_storage: dict[str, Any], ) -> None: """Test device removal.""" assert await async_setup_component(hass, "config", {}) - await knx.setup_integration({}) + await knx.setup_integration(config_store_fixture="config_store_light_switch.json") client = await hass_ws_client(hass) await knx.assert_read("1/0/21", response=True, ignore_order=True) # test light diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index e5f776a9404..e4a208906c6 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -28,7 +28,7 @@ async def test_if_fires_on_telegram( knx: KNXTestKit, ) -> None: """Test telegram device triggers firing.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -124,7 +124,7 @@ async def test_default_if_fires_on_telegram( # by default (without a user changing any) extra_fields are not added to the trigger and # pre 2024.2 device triggers did only support "destination" field so they didn't have # "group_value_write", "group_value_response", "group_value_read", "incoming", "outgoing" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -206,7 +206,7 @@ async def test_remove_device_trigger( ) -> None: """Test for removed callback when device trigger not used.""" automation_name = "telegram_trigger_automation" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -256,7 +256,7 @@ async def test_get_triggers( knx: KNXTestKit, ) -> None: """Test we get the expected device triggers from knx.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -279,7 +279,7 @@ async def test_get_trigger_capabilities( knx: KNXTestKit, ) -> None: """Test we get the expected capabilities telegram device trigger.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -361,7 +361,7 @@ async def test_invalid_device_trigger( caplog: pytest.LogCaptureFixture, ) -> None: """Test invalid telegram device trigger configuration.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) @@ -404,7 +404,7 @@ async def test_invalid_trigger_configuration( knx: KNXTestKit, ) -> None: """Test invalid telegram device trigger configuration at attach_trigger.""" - await knx.setup_integration({}) + await knx.setup_integration() device_entry = device_registry.async_get_device( identifiers={(DOMAIN, f"_{knx.mock_config_entry.entry_id}_interface")} ) diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index bb60e66f7e7..6d4bf7e6007 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -1,5 +1,7 @@ """Tests for the diagnostics data provided by the KNX integration.""" +from typing import Any + import pytest from syrupy import SnapshotAssertion from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT @@ -40,7 +42,7 @@ async def test_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await knx.setup_integration({}) + await knx.setup_integration() # Overwrite the version for this test since we don't want to change this with every library bump knx.xknx.version = "0.0.0" @@ -60,7 +62,7 @@ async def test_diagnostic_config_error( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await knx.setup_integration({}) + await knx.setup_integration() # Overwrite the version for this test since we don't want to change this with every library bump knx.xknx.version = "0.0.0" @@ -76,6 +78,7 @@ async def test_diagnostic_config_error( async def test_diagnostic_redact( hass: HomeAssistant, hass_client: ClientSessionGenerator, + hass_storage: dict[str, Any], snapshot: SnapshotAssertion, ) -> None: """Test diagnostics redacting data.""" @@ -95,8 +98,8 @@ async def test_diagnostic_redact( CONF_KNX_ROUTING_BACKBONE_KEY: "bbaacc44bbaacc44bbaacc44bbaacc44", }, ) - knx: KNXTestKit = KNXTestKit(hass, mock_config_entry) - await knx.setup_integration({}) + knx: KNXTestKit = KNXTestKit(hass, mock_config_entry, hass_storage) + await knx.setup_integration() # Overwrite the version for this test since we don't want to change this with every library bump knx.xknx.version = "0.0.0" @@ -117,7 +120,7 @@ async def test_diagnostics_project( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await knx.setup_integration({}) + await knx.setup_integration() knx.xknx.version = "0.0.0" # snapshot will contain project specific fields in `project_info` assert ( diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 48cc46ef1ee..579f9b143a2 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -1,7 +1,9 @@ """Test KNX init.""" +from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from xknx.io import ( DEFAULT_MCAST_GRP, @@ -11,7 +13,10 @@ from xknx.io import ( SecureConfig, ) -from homeassistant.components.knx.config_flow import DEFAULT_ROUTING_IA +from homeassistant.components.knx.config_flow import ( + DEFAULT_ENTRY_DATA, + DEFAULT_ROUTING_IA, +) from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, @@ -40,12 +45,13 @@ from homeassistant.components.knx.const import ( KNXConfigEntryData, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant +from . import KnxEntityGenerator from .conftest import KNXTestKit -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize( @@ -220,7 +226,7 @@ async def test_init_connection_handling( data=config_entry_data, ) knx.mock_config_entry = config_entry - await knx.setup_integration({}) + await knx.setup_integration() assert hass.data.get(KNX_DOMAIN) is not None @@ -262,6 +268,79 @@ async def test_init_connection_handling( ) +async def _init_switch_and_wait_for_first_state_updater_run( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + freezer: FrozenDateTimeFactory, + config_entry_data: KNXConfigEntryData, +) -> None: + """Return a config entry with default data.""" + config_entry = MockConfigEntry( + title="KNX", domain=KNX_DOMAIN, data=config_entry_data + ) + knx.mock_config_entry = config_entry + await knx.setup_integration() + await create_ui_entity( + platform=Platform.SWITCH, + knx_data={ + "ga_switch": {"write": "1/1/1", "state": "2/2/2"}, + "respond_to_read": True, + "sync_state": True, # True uses xknx default state updater + "invert": False, + }, + ) + # created entity sends read-request to KNX bus on connection + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", True) + + freezer.tick(timedelta(minutes=59)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.async_block_till_done() + await knx.assert_no_telegram() + + freezer.tick(timedelta(minutes=1)) # 60 minutes passed + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.async_block_till_done() + + +async def test_default_state_updater_enabled( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test default state updater is applied to xknx device instances.""" + config_entry = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + connection_type=CONF_KNX_AUTOMATIC, # missing in default data + state_updater=True, + ) + await _init_switch_and_wait_for_first_state_updater_run( + hass, knx, create_ui_entity, freezer, config_entry + ) + await knx.assert_read("2/2/2") + await knx.receive_response("2/2/2", True) + + +async def test_default_state_updater_disabled( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test default state updater is applied to xknx device instances.""" + config_entry = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + connection_type=CONF_KNX_AUTOMATIC, # missing in default data + state_updater=False, + ) + await _init_switch_and_wait_for_first_state_updater_run( + hass, knx, create_ui_entity, freezer, config_entry + ) + await knx.assert_no_telegram() + + async def test_async_remove_entry( hass: HomeAssistant, knx: KNXTestKit, @@ -275,14 +354,14 @@ async def test_async_remove_entry( }, ) knx.mock_config_entry = config_entry - await knx.setup_integration({}) + await knx.setup_integration() with ( patch("pathlib.Path.unlink") as unlink_mock, patch("pathlib.Path.rmdir") as rmdir_mock, ): assert await hass.config_entries.async_remove(config_entry.entry_id) - assert unlink_mock.call_count == 3 + assert unlink_mock.call_count == 4 rmdir_mock.assert_called_once() assert hass.config_entries.async_entries() == [] diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 79114d4ffd5..4de366c69f0 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -25,7 +25,7 @@ async def test_diagnostic_entities( freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostic entities.""" - await knx.setup_integration({}) + await knx.setup_integration() for entity_id in ( "sensor.knx_interface_individual_address", @@ -103,7 +103,7 @@ async def test_removed_entity( with patch( "xknx.core.connection_manager.ConnectionManager.unregister_connection_state_changed_cb" ) as unregister_mock: - await knx.setup_integration({}) + await knx.setup_integration() entity_registry.async_update_entity( "sensor.knx_interface_connection_established", @@ -120,7 +120,7 @@ async def test_remove_interface_device( ) -> None: """Test device removal.""" assert await async_setup_component(hass, "config", {}) - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) knx_devices = device_registry.devices.get_devices_for_config_entry_id( knx.mock_config_entry.entry_id diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 6ba6090d60d..fb0246763a4 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -1176,7 +1176,7 @@ async def test_light_ui_create( create_ui_entity: KnxEntityGenerator, ) -> None: """Test creating a light.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.LIGHT, entity_data={"name": "test"}, @@ -1213,7 +1213,7 @@ async def test_light_ui_color_temp( raw_ct: tuple[int, ...], ) -> None: """Test creating a color-temp light.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.LIGHT, entity_data={"name": "test"}, @@ -1250,7 +1250,7 @@ async def test_light_ui_multi_mode( create_ui_entity: KnxEntityGenerator, ) -> None: """Test creating a light with multiple color modes.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.LIGHT, entity_data={"name": "test"}, @@ -1335,13 +1335,11 @@ async def test_light_ui_multi_mode( async def test_light_ui_load( - hass: HomeAssistant, knx: KNXTestKit, - load_config_store: None, entity_registry: er.EntityRegistry, ) -> None: """Test loading a light from storage.""" - await knx.setup_integration({}) + await knx.setup_integration(config_store_fixture="config_store_light_switch.json") await knx.assert_read("1/0/21", response=True, ignore_order=True) # unrelated switch in config store diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index f70389dbc92..c4b48b5e81d 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -111,7 +111,7 @@ async def test_send( expected_apci, ) -> None: """Test `knx.send` service.""" - await knx.setup_integration({}) + await knx.setup_integration() await hass.services.async_call( "knx", @@ -127,7 +127,7 @@ async def test_send( async def test_read(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test `knx.read` service.""" - await knx.setup_integration({}) + await knx.setup_integration() # send read telegram await hass.services.async_call("knx", "read", {"address": "1/1/1"}, blocking=True) @@ -150,7 +150,7 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: events = async_capture_events(hass, "knx_event") test_address = "1/2/3" - await knx.setup_integration({}) + await knx.setup_integration() # no event registered await knx.receive_write(test_address, True) @@ -200,7 +200,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: test_entity = "fake.entity" test_attribute = "fake_attribute" - await knx.setup_integration({}) + await knx.setup_integration() # no exposure registered hass.states.async_set(test_entity, STATE_ON, {}) @@ -265,7 +265,7 @@ async def test_reload_service( knx: KNXTestKit, ) -> None: """Test reload service.""" - await knx.setup_integration({}) + await knx.setup_integration() with ( patch( @@ -285,7 +285,7 @@ async def test_reload_service( async def test_service_setup_failed(hass: HomeAssistant, knx: KNXTestKit) -> None: """Test service setup failed.""" - await knx.setup_integration({}) + await knx.setup_integration() await hass.config_entries.async_unload(knx.mock_config_entry.entry_id) with pytest.raises(HomeAssistantError) as exc_info: diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index bc0a6b27675..969c11b8e1a 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -155,7 +155,7 @@ async def test_switch_ui_create( create_ui_entity: KnxEntityGenerator, ) -> None: """Test creating a switch.""" - await knx.setup_integration({}) + await knx.setup_integration() await create_ui_entity( platform=Platform.SWITCH, entity_data={"name": "test"}, @@ -171,3 +171,16 @@ async def test_switch_ui_create( await knx.receive_response("2/2/2", True) state = hass.states.get("switch.test") assert state.state is STATE_ON + + +async def test_switch_ui_load(knx: KNXTestKit) -> None: + """Test loading a switch from storage.""" + await knx.setup_integration(config_store_fixture="config_store_light_switch.json") + + await knx.assert_read("1/0/45", response=True, ignore_order=True) + # unrelated light in config store + await knx.assert_read("1/0/21", response=True, ignore_order=True) + knx.assert_state( + "switch.none_test", # has_entity_name with unregistered device -> none_test + STATE_ON, + ) diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py index 883e8ccbb2d..840959bb6c5 100644 --- a/tests/components/knx/test_telegrams.py +++ b/tests/components/knx/test_telegrams.py @@ -70,7 +70,7 @@ async def test_store_telegam_history( hass_storage: dict[str, Any], ) -> None: """Test storing telegram history.""" - await knx.setup_integration({}) + await knx.setup_integration() await knx.receive_write("1/3/4", True) await hass.services.async_call( @@ -94,7 +94,7 @@ async def test_load_telegam_history( ) -> None: """Test telegram history restoration.""" hass_storage["knx/telegrams_history.json"] = {"version": 1, "data": MOCK_TELEGRAMS} - await knx.setup_integration({}) + await knx.setup_integration() loaded_telegrams = hass.data[KNX_MODULE_KEY].telegrams.recent_telegrams assert assert_telegram_history(loaded_telegrams) # TelegramDict "payload" is a tuple, this shall be restored when loading from JSON @@ -113,7 +113,7 @@ async def test_remove_telegam_history( knx.mock_config_entry, data=knx.mock_config_entry.data | {CONF_KNX_TELEGRAM_LOG_SIZE: 0}, ) - await knx.setup_integration({}, add_entry_to_hass=False) + await knx.setup_integration(add_entry_to_hass=False) # Store.async_remove() is mocked by hass_storage - check that data was removed. assert "knx/telegrams_history.json" not in hass_storage assert not hass.data[KNX_MODULE_KEY].telegrams.recent_telegrams diff --git a/tests/components/knx/test_trigger.py b/tests/components/knx/test_trigger.py index 73e8b10840e..1ce42a23482 100644 --- a/tests/components/knx/test_trigger.py +++ b/tests/components/knx/test_trigger.py @@ -18,7 +18,7 @@ async def test_telegram_trigger( knx: KNXTestKit, ) -> None: """Test telegram triggers firing.""" - await knx.setup_integration({}) + await knx.setup_integration() # "id" field added to action to test if `trigger_data` passed correctly in `async_attach_trigger` assert await async_setup_component( @@ -105,7 +105,7 @@ async def test_telegram_trigger_dpt_option( expected_unit: str | None, ) -> None: """Test telegram trigger type option.""" - await knx.setup_integration({}) + await knx.setup_integration() assert await async_setup_component( hass, automation.DOMAIN, @@ -190,7 +190,7 @@ async def test_telegram_trigger_options( direction_options: dict[str, bool], ) -> None: """Test telegram trigger options.""" - await knx.setup_integration({}) + await knx.setup_integration() assert await async_setup_component( hass, automation.DOMAIN, @@ -266,7 +266,7 @@ async def test_remove_telegram_trigger( ) -> None: """Test for removed callback when telegram trigger not used.""" automation_name = "telegram_trigger_automation" - await knx.setup_integration({}) + await knx.setup_integration() assert await async_setup_component( hass, @@ -311,7 +311,7 @@ async def test_invalid_trigger( caplog: pytest.LogCaptureFixture, ) -> None: """Test invalid telegram trigger configuration.""" - await knx.setup_integration({}) + await knx.setup_integration() caplog.clear() with caplog.at_level(logging.ERROR): assert await async_setup_component( diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index a34f126e4f4..7054d415ee9 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -20,7 +20,7 @@ async def test_knx_info_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: """Test knx/info command.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/info"}) @@ -39,7 +39,7 @@ async def test_knx_info_command_with_project( load_knxproj: None, ) -> None: """Test knx/info command with loaded project.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/info"}) @@ -65,7 +65,7 @@ async def test_knx_project_file_process( _password = "pw-test" _parse_result = FIXTURE_PROJECT_DATA - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) assert not hass.data[KNX_MODULE_KEY].project.loaded @@ -100,7 +100,7 @@ async def test_knx_project_file_process_error( hass_ws_client: WebSocketGenerator, ) -> None: """Test knx/project_file_process exception handling.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) assert not hass.data[KNX_MODULE_KEY].project.loaded @@ -134,7 +134,7 @@ async def test_knx_project_file_remove( hass_storage: dict[str, Any], ) -> None: """Test knx/project_file_remove command.""" - await knx.setup_integration({}) + await knx.setup_integration() assert hass_storage[KNX_PROJECT_STORAGE_KEY] client = await hass_ws_client(hass) assert hass.data[KNX_MODULE_KEY].project.loaded @@ -154,7 +154,7 @@ async def test_knx_get_project( load_knxproj: None, ) -> None: """Test retrieval of kxnproject from store.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) assert hass.data[KNX_MODULE_KEY].project.loaded @@ -169,7 +169,7 @@ async def test_knx_group_monitor_info_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: """Test knx/group_monitor_info command.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/group_monitor_info"}) @@ -184,7 +184,7 @@ async def test_knx_group_telegrams_command( hass: HomeAssistant, knx: KNXTestKit, hass_ws_client: WebSocketGenerator ) -> None: """Test knx/group_telegrams command.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json_auto_id({"type": "knx/group_telegrams"}) @@ -338,7 +338,7 @@ async def test_knx_subscribe_telegrams_command_project( load_knxproj: None, ) -> None: """Test knx/subscribe_telegrams command with project data.""" - await knx.setup_integration({}) + await knx.setup_integration() client = await hass_ws_client(hass) await client.send_json({"id": 6, "type": "knx/subscribe_telegrams"}) res = await client.receive_json() @@ -405,7 +405,7 @@ async def test_websocket_when_config_entry_unloaded( endpoint: str, ) -> None: """Test websocket connection when config entry is unloaded.""" - await knx.setup_integration({}) + await knx.setup_integration() await hass.config_entries.async_unload(knx.mock_config_entry.entry_id) client = await hass_ws_client(hass) diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index e56ba03b7e5..cc8acbaebf6 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -2,8 +2,8 @@ from ipaddress import ip_address -from homeassistant.components import zeroconf from homeassistant.components.kodi.const import DEFAULT_SSL +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo TEST_HOST = { "host": "1.1.1.1", @@ -17,7 +17,7 @@ TEST_CREDENTIALS = {"username": "username", "password": "password"} TEST_WS_PORT = {"ws_port": 9090} UUID = "11111111-1111-1111-1111-111111111111" -TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( +TEST_DISCOVERY = ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], port=8080, @@ -28,7 +28,7 @@ TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( ) -TEST_DISCOVERY_WO_UUID = zeroconf.ZeroconfServiceInfo( +TEST_DISCOVERY_WO_UUID = ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], port=8080, diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 5865616c544..c9fa70de256 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import konnected, ssdp +from homeassistant.components import konnected from homeassistant.components.konnected import config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from tests.common import MockConfigEntry @@ -116,7 +117,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/Device.xml", @@ -141,7 +142,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/Device.xml", @@ -160,7 +161,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/Device.xml", @@ -175,7 +176,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/Device.xml", @@ -193,7 +194,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/Device.xml", @@ -217,7 +218,7 @@ async def test_ssdp(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/Device.xml", @@ -343,7 +344,7 @@ async def test_import_ssdp_host_user_finish(hass: HomeAssistant, mock_panel) -> ssdp_result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://0.0.0.0:1234/Device.xml", @@ -390,7 +391,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://0.0.0.0:1234/Device.xml", @@ -470,7 +471,7 @@ async def test_ssdp_host_update(hass: HomeAssistant, mock_panel) -> None: result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.1.1.1:1234/Device.xml", diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index a2245e721c5..230a2562282 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -32,7 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index 913f6c72f24..7221fa4c071 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -15,7 +15,13 @@ TEST_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2"}, "unit": "degrees_celsius"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -26,7 +32,13 @@ TEST_NO_PERMISSION_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2"}, "unit": "degrees_celsius"} + } + } + }, permissions={"read": False}, model="Test", ) @@ -37,7 +49,16 @@ TEST_UNSUPPORTED_SENSOR = Sensor( sensor_id="2", sensor_field_names=["SomeUnsupportedField"], location=Location(id="1", name="Test"), - data={"SomeUnsupportedField": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "SomeUnsupportedField": { + "spot": {"value": "2"}, + "unit": "degrees_celsius", + } + } + } + }, permissions={"read": True}, model="Test", ) @@ -48,7 +69,13 @@ TEST_FLOAT_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2.3"}], "unit": "degrees_celsius"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2.3"}, "unit": "degrees_celsius"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -59,7 +86,9 @@ TEST_STRING_SENSOR = Sensor( sensor_id="2", sensor_field_names=["WetDry"], location=Location(id="1", name="Test"), - data={"WetDry": {"values": [{"s": "dry"}], "unit": "wet_dry"}}, + data={ + "data": {"current": {"WetDry": {"spot": {"value": "dry"}, "unit": "wet_dry"}}} + }, permissions={"read": True}, model="Test", ) @@ -70,7 +99,13 @@ TEST_ALREADY_FLOAT_SENSOR = Sensor( sensor_id="2", sensor_field_names=["HeatIndex"], location=Location(id="1", name="Test"), - data={"HeatIndex": {"values": [{"s": 2.3}], "unit": "degrees_fahrenheit"}}, + data={ + "data": { + "current": { + "HeatIndex": {"spot": {"value": 2.3}, "unit": "degrees_fahrenheit"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -81,7 +116,13 @@ TEST_ALREADY_INT_SENSOR = Sensor( sensor_id="2", sensor_field_names=["WindSpeed"], location=Location(id="1", name="Test"), - data={"WindSpeed": {"values": [{"s": 2}], "unit": "kilometers_per_hour"}}, + data={ + "data": { + "current": { + "WindSpeed": {"spot": {"value": 2}, "unit": "kilometers_per_hour"} + } + } + }, permissions={"read": True}, model="Test", ) @@ -92,7 +133,7 @@ TEST_NO_FIELD_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={}, + data={"data": {"current": {}}}, permissions={"read": True}, model="Test", ) @@ -103,7 +144,7 @@ TEST_MISSING_FIELD_DATA_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": None}, + data={"data": {"current": {"Temperature": None}}}, permissions={"read": True}, model="Test", ) @@ -114,7 +155,35 @@ TEST_UNITS_OVERRIDE_SENSOR = Sensor( sensor_id="2", sensor_field_names=["Temperature"], location=Location(id="1", name="Test"), - data={"Temperature": {"values": [{"s": "2.1"}], "unit": "degrees_fahrenheit"}}, + data={ + "data": { + "current": { + "Temperature": {"spot": {"value": "2.1"}, "unit": "degrees_fahrenheit"} + } + } + }, + permissions={"read": True}, + model="Test", +) +TEST_NO_READINGS_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"error": "no_readings"}, + permissions={"read": True}, + model="Test", +) +TEST_OTHER_ERROR_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"error": "some_other_error"}, permissions={"read": True}, model="Test", ) diff --git a/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr b/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr index 640726e2355..0975704b680 100644 --- a/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr +++ b/tests/components/lacrosse_view/snapshots/test_diagnostics.ambr @@ -4,7 +4,7 @@ 'coordinator_data': list([ dict({ '__type': "", - 'repr': "Sensor(name='Test', device_id='1', type='Test', sensor_id='2', sensor_field_names=['Temperature'], location=Location(id='1', name='Test'), permissions={'read': True}, model='Test', data={'Temperature': {'values': [{'s': '2'}], 'unit': 'degrees_celsius'}})", + 'repr': "Sensor(name='Test', device_id='1', type='Test', sensor_id='2', sensor_field_names=['Temperature'], location=Location(id='1', name='Test'), permissions={'read': True}, model='Test', data={'Temperature': {'spot': {'value': '2'}, 'unit': 'degrees_celsius'}})", }), ]), 'entry': dict({ diff --git a/tests/components/lacrosse_view/test_diagnostics.py b/tests/components/lacrosse_view/test_diagnostics.py index dc48f160113..4306173c6b3 100644 --- a/tests/components/lacrosse_view/test_diagnostics.py +++ b/tests/components/lacrosse_view/test_diagnostics.py @@ -26,9 +26,14 @@ async def test_entry_diagnostics( ) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch("lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR]), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[sensor]), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index 51fa7e5abf4..0533dd2abee 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -20,12 +20,17 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -68,7 +73,7 @@ async def test_http_error(hass: HomeAssistant) -> None: with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch("lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError), + patch("lacrosse_view.LaCrosse.get_devices", side_effect=HTTPError), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -78,18 +83,40 @@ async def test_http_error(hass: HomeAssistant) -> None: assert len(entries) == 1 assert entries[0].state is ConfigEntryState.SETUP_RETRY + config_entry_2 = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry_2.add_to_hass(hass) + + # Start over, let get_devices succeed but get_sensor_status fail + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[TEST_SENSOR]), + patch("lacrosse_view.LaCrosse.get_sensor_status", side_effect=HTTPError), + ): + assert not await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 2 + assert entries[1].state is ConfigEntryState.SETUP_RETRY + async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test new token.""" config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -103,7 +130,7 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", + "lacrosse_view.LaCrosse.get_devices", return_value=[TEST_SENSOR], ), ): @@ -121,12 +148,17 @@ async def test_failed_token( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 11faaf8877e..e0dc1e5f35f 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -18,6 +18,8 @@ from . import ( TEST_MISSING_FIELD_DATA_SENSOR, TEST_NO_FIELD_SENSOR, TEST_NO_PERMISSION_SENSOR, + TEST_NO_READINGS_SENSOR, + TEST_OTHER_ERROR_SENSOR, TEST_SENSOR, TEST_STRING_SENSOR, TEST_UNITS_OVERRIDE_SENSOR, @@ -32,9 +34,14 @@ async def test_entities_added(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch("lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR]), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[sensor]), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -54,12 +61,17 @@ async def test_sensor_permission( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_NO_PERMISSION_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_NO_PERMISSION_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -79,11 +91,14 @@ async def test_field_not_supported( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_UNSUPPORTED_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), - patch( - "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_UNSUPPORTED_SENSOR] - ), + patch("lacrosse_view.LaCrosse.get_devices", return_value=[sensor]), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -104,7 +119,7 @@ async def test_field_not_supported( (TEST_STRING_SENSOR, "dry", "wet_dry"), (TEST_ALREADY_FLOAT_SENSOR, "-16.5", "heat_index"), (TEST_ALREADY_INT_SENSOR, "2", "wind_speed"), - (TEST_UNITS_OVERRIDE_SENSOR, "-16.6", "temperature"), + (TEST_UNITS_OVERRIDE_SENSOR, "-16.6111111111111", "temperature"), ], ) async def test_field_types( @@ -114,12 +129,17 @@ async def test_field_types( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = test_input.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", + "lacrosse_view.LaCrosse.get_devices", return_value=[test_input], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -137,12 +157,17 @@ async def test_no_field(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_NO_FIELD_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_NO_FIELD_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -160,12 +185,17 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) + sensor = TEST_MISSING_FIELD_DATA_SENSOR.model_copy() + status = sensor.data + sensor.data = None + with ( patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_MISSING_FIELD_DATA_SENSOR], + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -176,3 +206,57 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: assert len(entries) == 1 assert entries[0].state is ConfigEntryState.LOADED assert hass.states.get("sensor.test_temperature").state == "unknown" + + +async def test_no_readings(hass: HomeAssistant) -> None: + """Test behavior when there are no readings.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + sensor = TEST_NO_READINGS_SENSOR.model_copy() + status = sensor.data + sensor.data = None + + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], + ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + assert hass.states.get("sensor.test_temperature").state == "unavailable" + + +async def test_other_error(hass: HomeAssistant) -> None: + """Test behavior when there is an error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + sensor = TEST_OTHER_ERROR_SENSOR.model_copy() + status = sensor.data + sensor.data = None + + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_devices", + return_value=[sensor], + ), + patch("lacrosse_view.LaCrosse.get_sensor_status", return_value=status), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 997fa73604c..658e0dd96bc 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -135,7 +135,10 @@ def mock_lamarzocco(device_fixture: MachineModel) -> Generator[MagicMock]: serial_number=serial_number, name=serial_number, ) - config = load_json_object_fixture("config.json", DOMAIN) + if device_fixture == MachineModel.LINEA_MINI: + config = load_json_object_fixture("config_mini.json", DOMAIN) + else: + config = load_json_object_fixture("config.json", DOMAIN) statistics = json.loads(load_fixture("statistics.json", DOMAIN)) dummy_machine.parse_config(config) diff --git a/tests/components/lamarzocco/fixtures/config_mini.json b/tests/components/lamarzocco/fixtures/config_mini.json new file mode 100644 index 00000000000..22533a94872 --- /dev/null +++ b/tests/components/lamarzocco/fixtures/config_mini.json @@ -0,0 +1,116 @@ +{ + "version": "v1", + "preinfusionModesAvailable": ["ByDoseType"], + "machineCapabilities": [ + { + "family": "LINEA", + "groupsNumber": 1, + "coffeeBoilersNumber": 1, + "hasCupWarmer": false, + "steamBoilersNumber": 1, + "teaDosesNumber": 1, + "machineModes": ["BrewingMode", "StandBy"], + "schedulingType": "smartWakeUpSleep" + } + ], + "machine_sn": "Sn01239157", + "machine_hw": "0", + "isPlumbedIn": false, + "isBackFlushEnabled": false, + "standByTime": 0, + "tankStatus": true, + "settings": [], + "recipes": [ + { + "id": "Recipe1", + "dose_mode": "Mass", + "recipe_doses": [ + { "id": "A", "target": 32 }, + { "id": "B", "target": 45 } + ] + } + ], + "recipeAssignment": [ + { + "dose_index": "DoseA", + "recipe_id": "Recipe1", + "recipe_dose": "A", + "group": "Group1" + } + ], + "groupCapabilities": [ + { + "capabilities": { + "groupType": "AV_Group", + "groupNumber": "Group1", + "boilerId": "CoffeeBoiler1", + "hasScale": false, + "hasFlowmeter": false, + "numberOfDoses": 1 + }, + "doses": [ + { + "groupNumber": "Group1", + "doseIndex": "DoseA", + "doseType": "MassType", + "stopTarget": 32 + } + ], + "doseMode": { "groupNumber": "Group1", "brewingType": "ManualType" } + } + ], + "machineMode": "StandBy", + "teaDoses": { "DoseA": { "doseIndex": "DoseA", "stopTarget": 0 } }, + "scale": { + "connected": true, + "address": "44:b7:d0:74:5f:90", + "name": "LMZ-123A45", + "battery": 64 + }, + "boilers": [ + { "id": "SteamBoiler", "isEnabled": false, "target": 0, "current": 0 }, + { "id": "CoffeeBoiler1", "isEnabled": true, "target": 89, "current": 42 } + ], + "boilerTargetTemperature": { "SteamBoiler": 0, "CoffeeBoiler1": 89 }, + "preinfusionMode": { + "Group1": { + "groupNumber": "Group1", + "preinfusionStyle": "PreinfusionByDoseType" + } + }, + "preinfusionSettings": { + "mode": "TypeB", + "Group1": [ + { + "groupNumber": "Group1", + "doseType": "DoseA", + "preWetTime": 2, + "preWetHoldTime": 3 + } + ] + }, + "wakeUpSleepEntries": [ + { + "id": "T6aLl42", + "days": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + "steam": false, + "enabled": false, + "timeOn": "24:0", + "timeOff": "24:0" + } + ], + "smartStandBy": { "mode": "LastBrewing", "minutes": 10, "enabled": true }, + "clock": "2024-08-31T14:47:45", + "firmwareVersions": [ + { "name": "machine_firmware", "fw_version": "2.12" }, + { "name": "gateway_firmware", "fw_version": "v3.6-rc4" } + ] +} diff --git a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr index cda285a7106..6cd4e8cd5ae 100644 --- a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr @@ -1,25 +1,12 @@ # serializer version: 1 -# name: test_binary_sensors[GS012345_backflush_active-binary_sensor] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'running', - 'friendly_name': 'GS012345 Backflush active', - }), - 'context': , - 'entity_id': 'binary_sensor.gs012345_backflush_active', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[GS012345_backflush_active-entry] +# name: test_binary_sensors[binary_sensor.gs012345_backflush_active-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -46,27 +33,28 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensors[GS012345_brewing_active-binary_sensor] +# name: test_binary_sensors[binary_sensor.gs012345_backflush_active-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'running', - 'friendly_name': 'GS012345 Brewing active', + 'friendly_name': 'GS012345 Backflush active', }), 'context': , - 'entity_id': 'binary_sensor.gs012345_brewing_active', + 'entity_id': 'binary_sensor.gs012345_backflush_active', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensors[GS012345_brewing_active-entry] +# name: test_binary_sensors[binary_sensor.gs012345_brewing_active-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -93,27 +81,28 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensors[GS012345_water_tank_empty-binary_sensor] +# name: test_binary_sensors[binary_sensor.gs012345_brewing_active-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'GS012345 Water tank empty', + 'device_class': 'running', + 'friendly_name': 'GS012345 Brewing active', }), 'context': , - 'entity_id': 'binary_sensor.gs012345_water_tank_empty', + 'entity_id': 'binary_sensor.gs012345_brewing_active', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensors[GS012345_water_tank_empty-entry] +# name: test_binary_sensors[binary_sensor.gs012345_water_tank_empty-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -140,3 +129,65 @@ 'unit_of_measurement': None, }) # --- +# name: test_binary_sensors[binary_sensor.gs012345_water_tank_empty-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'GS012345 Water tank empty', + }), + 'context': , + 'entity_id': 'binary_sensor.gs012345_water_tank_empty', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_scale_connectivity[Linea Mini] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'LMZ-123A45 Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.lmz_123a45_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_scale_connectivity[Linea Mini].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.lmz_123a45_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'LM012345_connected', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/lamarzocco/snapshots/test_button.ambr b/tests/components/lamarzocco/snapshots/test_button.ambr index 64d47a11072..33aace5f97a 100644 --- a/tests/components/lamarzocco/snapshots/test_button.ambr +++ b/tests/components/lamarzocco/snapshots/test_button.ambr @@ -19,6 +19,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lamarzocco/snapshots/test_calendar.ambr b/tests/components/lamarzocco/snapshots/test_calendar.ambr index 729eed5879a..74847892cfa 100644 --- a/tests/components/lamarzocco/snapshots/test_calendar.ambr +++ b/tests/components/lamarzocco/snapshots/test_calendar.ambr @@ -90,6 +90,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -123,6 +124,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lamarzocco/snapshots/test_init.ambr b/tests/components/lamarzocco/snapshots/test_init.ambr index 519a9301bfd..4c210136bd2 100644 --- a/tests/components/lamarzocco/snapshots/test_init.ambr +++ b/tests/components/lamarzocco/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -39,3 +40,36 @@ 'via_device_id': None, }) # --- +# name: test_scale_device[Linea Mini] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'lamarzocco', + '44:b7:d0:74:5f:90', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Acaia', + 'model': 'Lunar', + 'model_id': 'Y.301', + 'name': 'LMZ-123A45', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': , + }) +# --- diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index b7e42bb425f..0748c9384a9 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -30,6 +30,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -87,6 +88,7 @@ 'step': 10, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +146,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -201,6 +204,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -258,6 +262,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -315,6 +320,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -657,7 +663,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1', + 'state': '3', }) # --- # name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini].1 @@ -672,6 +678,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -729,6 +736,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -771,7 +779,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1', + 'state': '3', }) # --- # name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini].1 @@ -786,6 +794,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -843,6 +852,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -885,7 +895,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1', + 'state': '3', }) # --- # name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Linea Mini].1 @@ -900,6 +910,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -957,6 +968,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -983,3 +995,115 @@ 'unit_of_measurement': , }) # --- +# name: test_set_target[Linea Mini-1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LMZ-123A45 Brew by weight target 1', + 'max': 100, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.lmz_123a45_brew_by_weight_target_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '32', + }) +# --- +# name: test_set_target[Linea Mini-1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.lmz_123a45_brew_by_weight_target_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Brew by weight target 1', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'scale_target_key', + 'unique_id': 'LM012345_scale_target_key1', + 'unit_of_measurement': None, + }) +# --- +# name: test_set_target[Linea Mini-2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LMZ-123A45 Brew by weight target 2', + 'max': 100, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.lmz_123a45_brew_by_weight_target_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45', + }) +# --- +# name: test_set_target[Linea Mini-2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.lmz_123a45_brew_by_weight_target_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Brew by weight target 2', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'scale_target_key', + 'unique_id': 'LM012345_scale_target_key2', + 'unit_of_measurement': None, + }) +# --- diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index 46fa55eff13..2e88688652a 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -1,4 +1,60 @@ # serializer version: 1 +# name: test_active_bbw_recipe[Linea Mini] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LMZ-123A45 Active brew by weight recipe', + 'options': list([ + 'a', + 'b', + ]), + }), + 'context': , + 'entity_id': 'select.lmz_123a45_active_brew_by_weight_recipe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'a', + }) +# --- +# name: test_active_bbw_recipe[Linea Mini].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'a', + 'b', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.lmz_123a45_active_brew_by_weight_recipe', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Active brew by weight recipe', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_bbw', + 'unique_id': 'LM012345_active_bbw', + 'unit_of_measurement': None, + }) +# --- # name: test_pre_brew_infusion_select[GS3 AV] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -30,6 +86,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -87,6 +144,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +202,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -199,6 +258,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -256,6 +316,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index da1efbf1eaa..996dff93433 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -1,5 +1,21 @@ # serializer version: 1 -# name: test_sensors[GS012345_current_coffee_temperature-entry] +# name: test_scale_battery[Linea Mini] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'LMZ-123A45 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.lmz_123a45_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '64', + }) +# --- +# name: test_scale_battery[Linea Mini].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -8,6 +24,247 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.lmz_123a45_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'LM012345_scale_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_coffees_made_key_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Coffees made Key 1', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drink_stats_coffee_key', + 'unique_id': 'GS012345_drink_stats_coffee_key_key1', + 'unit_of_measurement': 'coffees', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Coffees made Key 1', + 'state_class': , + 'unit_of_measurement': 'coffees', + }), + 'context': , + 'entity_id': 'sensor.gs012345_coffees_made_key_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1047', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_coffees_made_key_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Coffees made Key 2', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drink_stats_coffee_key', + 'unique_id': 'GS012345_drink_stats_coffee_key_key2', + 'unit_of_measurement': 'coffees', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Coffees made Key 2', + 'state_class': , + 'unit_of_measurement': 'coffees', + }), + 'context': , + 'entity_id': 'sensor.gs012345_coffees_made_key_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '560', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_coffees_made_key_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Coffees made Key 3', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drink_stats_coffee_key', + 'unique_id': 'GS012345_drink_stats_coffee_key_key3', + 'unit_of_measurement': 'coffees', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Coffees made Key 3', + 'state_class': , + 'unit_of_measurement': 'coffees', + }), + 'context': , + 'entity_id': 'sensor.gs012345_coffees_made_key_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '468', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_coffees_made_key_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Coffees made Key 4', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drink_stats_coffee_key', + 'unique_id': 'GS012345_drink_stats_coffee_key_key4', + 'unit_of_measurement': 'coffees', + }) +# --- +# name: test_sensors[sensor.gs012345_coffees_made_key_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Coffees made Key 4', + 'state_class': , + 'unit_of_measurement': 'coffees', + }), + 'context': , + 'entity_id': 'sensor.gs012345_coffees_made_key_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '312', + }) +# --- +# name: test_sensors[sensor.gs012345_current_coffee_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -37,7 +294,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[GS012345_current_coffee_temperature-sensor] +# name: test_sensors[sensor.gs012345_current_coffee_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -53,7 +310,7 @@ 'state': '96.5', }) # --- -# name: test_sensors[GS012345_current_steam_temperature-entry] +# name: test_sensors[sensor.gs012345_current_steam_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -62,6 +319,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -91,7 +349,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[GS012345_current_steam_temperature-sensor] +# name: test_sensors[sensor.gs012345_current_steam_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -107,7 +365,7 @@ 'state': '123.800003051758', }) # --- -# name: test_sensors[GS012345_shot_timer-entry] +# name: test_sensors[sensor.gs012345_shot_timer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -116,6 +374,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -142,7 +401,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[GS012345_shot_timer-sensor] +# name: test_sensors[sensor.gs012345_shot_timer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'duration', @@ -158,7 +417,7 @@ 'state': '0', }) # --- -# name: test_sensors[GS012345_total_coffees_made-entry] +# name: test_sensors[sensor.gs012345_total_coffees_made-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -167,6 +426,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,25 +450,25 @@ 'supported_features': 0, 'translation_key': 'drink_stats_coffee', 'unique_id': 'GS012345_drink_stats_coffee', - 'unit_of_measurement': 'drinks', + 'unit_of_measurement': 'coffees', }) # --- -# name: test_sensors[GS012345_total_coffees_made-sensor] +# name: test_sensors[sensor.gs012345_total_coffees_made-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS012345 Total coffees made', 'state_class': , - 'unit_of_measurement': 'drinks', + 'unit_of_measurement': 'coffees', }), 'context': , 'entity_id': 'sensor.gs012345_total_coffees_made', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1047', + 'state': '2387', }) # --- -# name: test_sensors[GS012345_total_flushes_made-entry] +# name: test_sensors[sensor.gs012345_total_flushes_made-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -217,6 +477,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -240,15 +501,15 @@ 'supported_features': 0, 'translation_key': 'drink_stats_flushing', 'unique_id': 'GS012345_drink_stats_flushing', - 'unit_of_measurement': 'drinks', + 'unit_of_measurement': 'flushes', }) # --- -# name: test_sensors[GS012345_total_flushes_made-sensor] +# name: test_sensors[sensor.gs012345_total_flushes_made-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS012345 Total flushes made', 'state_class': , - 'unit_of_measurement': 'drinks', + 'unit_of_measurement': 'flushes', }), 'context': , 'entity_id': 'sensor.gs012345_total_flushes_made', diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 79a305c998f..085d9a16125 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -39,6 +40,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -91,26 +93,14 @@ 'state': 'on', }) # --- -# name: test_switches[-set_power-kwargs0] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS012345', - }), - 'context': , - 'entity_id': 'switch.gs012345', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switches[-set_power-kwargs0].1 +# name: test_switches[switch.gs012345-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -137,26 +127,121 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[_smart_standby_enabled-set_smart_standby-kwargs2] +# name: test_switches[switch.gs012345-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS012345 Smart standby enabled', + 'friendly_name': 'GS012345', }), 'context': , - 'entity_id': 'switch.gs012345_smart_standby_enabled', + 'entity_id': 'switch.gs012345', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switches[_smart_standby_enabled-set_smart_standby-kwargs2].1 +# name: test_switches[switch.gs012345_auto_on_off_axfz5bj-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.gs012345_auto_on_off_axfz5bj', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto on/off (aXFz5bJ)', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off', + 'unique_id': 'GS012345_auto_on_off_aXFz5bJ', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.gs012345_auto_on_off_axfz5bj-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Auto on/off (aXFz5bJ)', + }), + 'context': , + 'entity_id': 'switch.gs012345_auto_on_off_axfz5bj', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.gs012345_auto_on_off_os2oswx-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.gs012345_auto_on_off_os2oswx', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto on/off (Os2OswX)', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off', + 'unique_id': 'GS012345_auto_on_off_Os2OswX', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.gs012345_auto_on_off_os2oswx-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Auto on/off (Os2OswX)', + }), + 'context': , + 'entity_id': 'switch.gs012345_auto_on_off_os2oswx', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.gs012345_smart_standby_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -183,26 +268,27 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[_steam_boiler-set_steam-kwargs1] +# name: test_switches[switch.gs012345_smart_standby_enabled-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'GS012345 Steam boiler', + 'friendly_name': 'GS012345 Smart standby enabled', }), 'context': , - 'entity_id': 'switch.gs012345_steam_boiler', + 'entity_id': 'switch.gs012345_smart_standby_enabled', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switches[_steam_boiler-set_steam-kwargs1].1 +# name: test_switches[switch.gs012345_steam_boiler-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -229,3 +315,16 @@ 'unit_of_measurement': None, }) # --- +# name: test_switches[switch.gs012345_steam_boiler-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS012345 Steam boiler', + }), + 'context': , + 'entity_id': 'switch.gs012345_steam_boiler', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr index 46fa4cff815..17d0528c3d8 100644 --- a/tests/components/lamarzocco/snapshots/test_update.ambr +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -1,37 +1,12 @@ # serializer version: 1 -# name: test_update_entites[gateway_firmware-gateway] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'auto_update': False, - 'device_class': 'firmware', - 'display_precision': 0, - 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', - 'friendly_name': 'GS012345 Gateway firmware', - 'in_progress': False, - 'installed_version': 'v3.1-rc4', - 'latest_version': 'v3.5-rc3', - 'release_summary': None, - 'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/', - 'skipped_version': None, - 'supported_features': , - 'title': None, - 'update_percentage': None, - }), - 'context': , - 'entity_id': 'update.gs012345_gateway_firmware', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_update_entites[gateway_firmware-gateway].1 +# name: test_update[update.gs012345_gateway_firmware-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,17 +33,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_update_entites[machine_firmware-machine] +# name: test_update[update.gs012345_gateway_firmware-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'auto_update': False, 'device_class': 'firmware', 'display_precision': 0, 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', - 'friendly_name': 'GS012345 Machine firmware', + 'friendly_name': 'GS012345 Gateway firmware', 'in_progress': False, - 'installed_version': '1.40', - 'latest_version': '1.55', + 'installed_version': 'v3.1-rc4', + 'latest_version': 'v3.5-rc3', 'release_summary': None, 'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/', 'skipped_version': None, @@ -77,20 +52,21 @@ 'update_percentage': None, }), 'context': , - 'entity_id': 'update.gs012345_machine_firmware', + 'entity_id': 'update.gs012345_gateway_firmware', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_update_entites[machine_firmware-machine].1 +# name: test_update[update.gs012345_machine_firmware-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -117,3 +93,29 @@ 'unit_of_measurement': None, }) # --- +# name: test_update[update.gs012345_machine_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/lamarzocco/icon.png', + 'friendly_name': 'GS012345 Machine firmware', + 'in_progress': False, + 'installed_version': '1.40', + 'latest_version': '1.55', + 'release_summary': None, + 'release_url': 'https://support-iot.lamarzocco.com/firmware-updates/', + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.gs012345_machine_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 956bfe90dd4..d50d0ad9f84 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -1,25 +1,22 @@ """Tests for La Marzocco binary sensors.""" from datetime import timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory +from pylamarzocco.const import MachineModel from pylamarzocco.exceptions import RequestNotSuccessful +from pylamarzocco.models import LaMarzoccoScale +import pytest from syrupy import SnapshotAssertion -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import async_init_integration -from tests.common import MockConfigEntry, async_fire_time_changed - -BINARY_SENSORS = ( - "brewing_active", - "backflush_active", - "water_tank_empty", -) +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_binary_sensors( @@ -31,19 +28,11 @@ async def test_binary_sensors( ) -> None: """Test the La Marzocco binary sensors.""" - await async_init_integration(hass, mock_config_entry) - - serial_number = mock_lamarzocco.serial_number - - for binary_sensor in BINARY_SENSORS: - state = hass.states.get(f"binary_sensor.{serial_number}_{binary_sensor}") - assert state - assert state == snapshot(name=f"{serial_number}_{binary_sensor}-binary_sensor") - - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry.device_id - assert entry == snapshot(name=f"{serial_number}_{binary_sensor}-entry") + with patch( + "homeassistant.components.lamarzocco.PLATFORMS", [Platform.BINARY_SENSOR] + ): + await async_init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) async def test_brew_active_does_not_exists( @@ -98,3 +87,68 @@ async def test_sensor_going_unavailable( state = hass.states.get(brewing_active_sensor) assert state assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_scale_connectivity( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the scale binary sensors.""" + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.lmz_123a45_connectivity") + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot + + +@pytest.mark.parametrize( + "device_fixture", + [MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA], +) +async def test_other_models_no_scale_connectivity( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure the other models don't have a connectivity sensor.""" + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.lmz_123a45_connectivity") + assert state is None + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_connectivity_on_new_scale_added( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure the connectivity binary sensor for a new scale is added automatically.""" + + mock_lamarzocco.config.scale = None + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("binary_sensor.scale_123a45_connectivity") + assert state is None + + mock_lamarzocco.config.scale = LaMarzoccoScale( + connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50 + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.scale_123a45_connectivity") + assert state diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index e25aab39012..02ade8f2b9c 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -8,7 +8,6 @@ from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from pylamarzocco.models import LaMarzoccoDeviceInfo import pytest -from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE from homeassistant.components.lamarzocco.const import CONF_USE_BLUETOOTH, DOMAIN from homeassistant.config_entries import ( @@ -28,6 +27,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import USER_INPUT, async_init_integration, get_bluetooth_service_info diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index 446c8780b62..09ebc462952 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -1,8 +1,10 @@ """Test initialization of lamarzocco.""" +from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch -from pylamarzocco.const import FirmwareType +from freezegun.api import FrozenDateTimeFactory +from pylamarzocco.const import FirmwareType, MachineModel from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful import pytest from syrupy import SnapshotAssertion @@ -27,7 +29,7 @@ from homeassistant.helpers import ( from . import USER_INPUT, async_init_integration, get_bluetooth_service_info -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_load_unload_config_entry( @@ -36,9 +38,7 @@ async def test_load_unload_config_entry( mock_lamarzocco: MagicMock, ) -> None: """Test loading and unloading the integration.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await async_init_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.LOADED @@ -56,9 +56,7 @@ async def test_config_entry_not_ready( """Test the La Marzocco configuration entry not ready.""" mock_lamarzocco.get_config.side_effect = RequestNotSuccessful("") - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await async_init_integration(hass, mock_config_entry) assert len(mock_lamarzocco.get_config.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -71,9 +69,7 @@ async def test_invalid_auth( ) -> None: """Test auth error during setup.""" mock_lamarzocco.get_config.side_effect = AuthFail("") - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await async_init_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert len(mock_lamarzocco.get_config.mock_calls) == 1 @@ -197,8 +193,11 @@ async def test_websocket_closed_on_unload( ) as local_client: client = local_client.return_value client.websocket = AsyncMock() - client.websocket.closed = False + await async_init_integration(hass, mock_config_entry) + mock_lamarzocco.websocket_connect.assert_called_once() + + client.websocket.closed = False hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() client.websocket.close.assert_called_once() @@ -251,3 +250,49 @@ async def test_device( device = device_registry.async_get(entry.device_id) assert device assert device == snapshot + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_scale_device( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the device.""" + + await async_init_integration(hass, mock_config_entry) + + device = device_registry.async_get_device( + identifiers={(DOMAIN, mock_lamarzocco.config.scale.address)} + ) + assert device + assert device == snapshot + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_remove_stale_scale( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure stale scale is cleaned up.""" + + await async_init_integration(hass, mock_config_entry) + + scale_address = mock_lamarzocco.config.scale.address + + device = device_registry.async_get_device(identifiers={(DOMAIN, scale_address)}) + assert device + + mock_lamarzocco.config.scale = None + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + device = device_registry.async_get_device(identifiers={(DOMAIN, scale_address)}) + assert device is None diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 710a0220e06..65c5e264f22 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -1,8 +1,10 @@ """Tests for the La Marzocco number entities.""" +from datetime import timedelta from typing import Any from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory from pylamarzocco.const import ( KEYS_PER_MODEL, BoilerType, @@ -11,6 +13,7 @@ from pylamarzocco.const import ( PrebrewMode, ) from pylamarzocco.exceptions import RequestNotSuccessful +from pylamarzocco.models import LaMarzoccoScale import pytest from syrupy import SnapshotAssertion @@ -26,7 +29,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from . import async_init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize( @@ -444,3 +447,91 @@ async def test_number_error( blocking=True, ) assert exc_info.value.translation_key == "number_exception_key" + + +@pytest.mark.parametrize("physical_key", [PhysicalKey.A, PhysicalKey.B]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_set_target( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + physical_key: PhysicalKey, +) -> None: + """Test the La Marzocco set target sensors.""" + + await async_init_integration(hass, mock_config_entry) + + entity_name = f"number.lmz_123a45_brew_by_weight_target_{int(physical_key)}" + + state = hass.states.get(entity_name) + + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot + + # service call + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_name, + ATTR_VALUE: 42, + }, + blocking=True, + ) + + mock_lamarzocco.set_bbw_recipe_target.assert_called_once_with(physical_key, 42) + + +@pytest.mark.parametrize( + "device_fixture", + [MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA], +) +async def test_other_models_no_scale_set_target( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure the other models don't have a set target numbers.""" + await async_init_integration(hass, mock_config_entry) + + for i in range(1, 3): + state = hass.states.get(f"number.lmz_123a45_brew_by_weight_target_{i}") + assert state is None + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_set_target_on_new_scale_added( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure the set target numbers for a new scale are added automatically.""" + + mock_lamarzocco.config.scale = None + await async_init_integration(hass, mock_config_entry) + + for i in range(1, 3): + state = hass.states.get(f"number.scale_123a45_brew_by_weight_target_{i}") + assert state is None + + mock_lamarzocco.config.scale = LaMarzoccoScale( + connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50 + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + for i in range(1, 3): + state = hass.states.get(f"number.scale_123a45_brew_by_weight_target_{i}") + assert state diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 24b96f84f37..3bfb579e6d4 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -1,9 +1,18 @@ """Tests for the La Marzocco select entities.""" +from datetime import timedelta from unittest.mock import MagicMock -from pylamarzocco.const import MachineModel, PrebrewMode, SmartStandbyMode, SteamLevel +from freezegun.api import FrozenDateTimeFactory +from pylamarzocco.const import ( + MachineModel, + PhysicalKey, + PrebrewMode, + SmartStandbyMode, + SteamLevel, +) from pylamarzocco.exceptions import RequestNotSuccessful +from pylamarzocco.models import LaMarzoccoScale import pytest from syrupy import SnapshotAssertion @@ -17,9 +26,14 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -pytestmark = pytest.mark.usefixtures("init_integration") +from . import async_init_integration + +from tests.common import MockConfigEntry, async_fire_time_changed + +pytest.mark.usefixtures("init_integration") +@pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MICRA]) async def test_steam_boiler_level( hass: HomeAssistant, @@ -69,6 +83,7 @@ async def test_steam_boiler_level_none( assert state is None +@pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize( "device_fixture", [MachineModel.LINEA_MICRA, MachineModel.GS3_AV, MachineModel.LINEA_MINI], @@ -106,6 +121,7 @@ async def test_pre_brew_infusion_select( mock_lamarzocco.set_prebrew_mode.assert_called_once_with(mode=PrebrewMode.PREBREW) +@pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize( "device_fixture", [MachineModel.GS3_MP], @@ -121,6 +137,7 @@ async def test_pre_brew_infusion_select_none( assert state is None +@pytest.mark.usefixtures("init_integration") async def test_smart_standby_mode( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -155,6 +172,7 @@ async def test_smart_standby_mode( ) +@pytest.mark.usefixtures("init_integration") async def test_select_errors( hass: HomeAssistant, mock_lamarzocco: MagicMock, @@ -179,3 +197,77 @@ async def test_select_errors( blocking=True, ) assert exc_info.value.translation_key == "select_option_error" + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_active_bbw_recipe( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_lamarzocco: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco active bbw recipe select.""" + + state = hass.states.get("select.lmz_123a45_active_brew_by_weight_recipe") + + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry == snapshot + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.lmz_123a45_active_brew_by_weight_recipe", + ATTR_OPTION: "b", + }, + blocking=True, + ) + + mock_lamarzocco.set_active_bbw_recipe.assert_called_once_with(PhysicalKey.B) + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + "device_fixture", + [MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA], +) +async def test_other_models_no_active_bbw_select( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, +) -> None: + """Ensure the other models don't have a battery sensor.""" + + state = hass.states.get("select.lmz_123a45_active_brew_by_weight_recipe") + assert state is None + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_active_bbw_select_on_new_scale_added( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure the active bbw select for a new scale is added automatically.""" + + mock_lamarzocco.config.scale = None + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("select.scale_123a45_active_brew_by_weight_recipe") + assert state is None + + mock_lamarzocco.config.scale = LaMarzoccoScale( + connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50 + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("select.scale_123a45_active_brew_by_weight_recipe") + assert state diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py index 6f14d52d1fc..43a0826d551 100644 --- a/tests/components/lamarzocco/test_sensor.py +++ b/tests/components/lamarzocco/test_sensor.py @@ -1,28 +1,24 @@ """Tests for La Marzocco sensors.""" -from unittest.mock import MagicMock +from datetime import timedelta +from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory from pylamarzocco.const import MachineModel +from pylamarzocco.models import LaMarzoccoScale import pytest from syrupy import SnapshotAssertion -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import async_init_integration -from tests.common import MockConfigEntry - -SENSORS = ( - "total_coffees_made", - "total_flushes_made", - "shot_timer", - "current_coffee_temperature", - "current_steam_temperature", -) +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, mock_lamarzocco: MagicMock, @@ -32,19 +28,9 @@ async def test_sensors( ) -> None: """Test the La Marzocco sensors.""" - serial_number = mock_lamarzocco.serial_number - - await async_init_integration(hass, mock_config_entry) - - for sensor in SENSORS: - state = hass.states.get(f"sensor.{serial_number}_{sensor}") - assert state - assert state == snapshot(name=f"{serial_number}_{sensor}-sensor") - - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry.device_id - assert entry == snapshot(name=f"{serial_number}_{sensor}-entry") + with patch("homeassistant.components.lamarzocco.PLATFORMS", [Platform.SENSOR]): + await async_init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) async def test_shot_timer_not_exists( @@ -85,3 +71,68 @@ async def test_no_steam_linea_mini( serial_number = mock_lamarzocco.serial_number state = hass.states.get(f"sensor.{serial_number}_current_temp_steam") assert state is None + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_scale_battery( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the scale battery sensor.""" + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.lmz_123a45_battery") + assert state + assert state == snapshot + + entry = entity_registry.async_get(state.entity_id) + assert entry + assert entry.device_id + assert entry == snapshot + + +@pytest.mark.parametrize( + "device_fixture", + [MachineModel.GS3_AV, MachineModel.GS3_MP, MachineModel.LINEA_MICRA], +) +async def test_other_models_no_scale_battery( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Ensure the other models don't have a battery sensor.""" + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.lmz_123a45_battery") + assert state is None + + +@pytest.mark.parametrize("device_fixture", [MachineModel.LINEA_MINI]) +async def test_battery_on_new_scale_added( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure the battery sensor for a new scale is added automatically.""" + + mock_lamarzocco.config.scale = None + await async_init_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.lmz_123a45_battery") + assert state is None + + mock_lamarzocco.config.scale = LaMarzoccoScale( + connected=True, name="Scale-123A45", address="aa:bb:cc:dd:ee:ff", battery=50 + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.scale_123a45_battery") + assert state diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index 9082e6f4c09..d8370ad8575 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -1,7 +1,7 @@ """Tests for La Marzocco switches.""" from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from pylamarzocco.exceptions import RequestNotSuccessful import pytest @@ -12,14 +12,27 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import WAKE_UP_SLEEP_ENTRY_IDS, async_init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_switches( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco switches.""" + with patch("homeassistant.components.lamarzocco.PLATFORMS", [Platform.SWITCH]): + await async_init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( @@ -38,12 +51,10 @@ from tests.common import MockConfigEntry ), ], ) -async def test_switches( +async def test_switches_actions( hass: HomeAssistant, mock_lamarzocco: MagicMock, mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, entity_name: str, method_name: str, kwargs: dict[str, Any], @@ -55,14 +66,6 @@ async def test_switches( control_fn = getattr(mock_lamarzocco, method_name) - state = hass.states.get(f"switch.{serial_number}{entity_name}") - assert state - assert state == snapshot - - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry == snapshot - await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index aef37d7c921..4089ffa297a 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -1,6 +1,6 @@ """Tests for the La Marzocco Update Entities.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from pylamarzocco.const import FirmwareType from pylamarzocco.exceptions import RequestNotSuccessful @@ -8,12 +8,27 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -pytestmark = pytest.mark.usefixtures("init_integration") +from . import async_init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_update( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the La Marzocco updates.""" + with patch("homeassistant.components.lamarzocco.PLATFORMS", [Platform.UPDATE]): + await async_init_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) @pytest.mark.parametrize( @@ -26,8 +41,7 @@ pytestmark = pytest.mark.usefixtures("init_integration") async def test_update_entites( hass: HomeAssistant, mock_lamarzocco: MagicMock, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, entity_name: str, component: FirmwareType, ) -> None: @@ -35,13 +49,7 @@ async def test_update_entites( serial_number = mock_lamarzocco.serial_number - state = hass.states.get(f"update.{serial_number}_{entity_name}") - assert state - assert state == snapshot - - entry = entity_registry.async_get(state.entity_id) - assert entry - assert entry == snapshot + await async_init_integration(hass, mock_config_entry) await hass.services.async_call( UPDATE_DOMAIN, @@ -65,10 +73,14 @@ async def test_update_entites( async def test_update_error( hass: HomeAssistant, mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, attr: str, value: bool | Exception, ) -> None: """Test error during update.""" + + await async_init_integration(hass, mock_config_entry) + state = hass.states.get(f"update.{mock_lamarzocco.serial_number}_machine_firmware") assert state diff --git a/tests/components/lametric/fixtures/computer_powered.json b/tests/components/lametric/fixtures/computer_powered.json new file mode 100644 index 00000000000..0465dd4dd3a --- /dev/null +++ b/tests/components/lametric/fixtures/computer_powered.json @@ -0,0 +1,68 @@ +{ + "audio": { + "available": true, + "volume": 53, + "volume_limit": { + "max": 100, + "min": 0 + }, + "volume_range": { + "max": 100, + "min": 0 + } + }, + "bluetooth": { + "active": false, + "address": "40:F4:C9:AA:AA:AA", + "available": true, + "discoverable": true, + "mac": "40:F4:C9:AA:AA:AA", + "name": "LM8367", + "pairable": false + }, + "display": { + "brightness": 75, + "brightness_limit": { + "max": 76, + "min": 2 + }, + "brightness_mode": "manual", + "brightness_range": { + "max": 100, + "min": 0 + }, + "height": 8, + "on": true, + "screensaver": { + "enabled": true, + "modes": { + "time_based": { + "enabled": false + }, + "when_dark": { + "enabled": true + } + }, + "widget": "1_com.lametric.clock" + }, + "type": "mixed", + "width": 37 + }, + "id": "67790", + "mode": "manual", + "model": "sa8", + "name": "TIME", + "os_version": "3.1.3", + "serial_number": "SA840700836700W00BAA", + "wifi": { + "active": true, + "mac": "40:F4:C9:AA:AA:AA", + "available": true, + "encryption": "WPA", + "ssid": "My wifi", + "ip": "10.0.0.99", + "mode": "dhcp", + "netmask": "255.255.255.0", + "rssi": 78 + } +} diff --git a/tests/components/lametric/snapshots/test_diagnostics.ambr b/tests/components/lametric/snapshots/test_diagnostics.ambr index 7517cfe035e..d8f21424216 100644 --- a/tests/components/lametric/snapshots/test_diagnostics.ambr +++ b/tests/components/lametric/snapshots/test_diagnostics.ambr @@ -2,6 +2,7 @@ # name: test_diagnostics dict({ 'audio': dict({ + 'available': True, 'volume': 100, 'volume_limit': dict({ 'range_max': 100, @@ -23,7 +24,15 @@ 'device_id': '**REDACTED**', 'display': dict({ 'brightness': 100, + 'brightness_limit': dict({ + 'range_max': 100, + 'range_min': 2, + }), 'brightness_mode': 'auto', + 'brightness_range': dict({ + 'range_max': 100, + 'range_min': 0, + }), 'display_type': 'mixed', 'height': 8, 'on': None, diff --git a/tests/components/lametric/test_button.py b/tests/components/lametric/test_button.py index 04efeaac87f..cc8c1379fe0 100644 --- a/tests/components/lametric/test_button.py +++ b/tests/components/lametric/test_button.py @@ -52,6 +52,7 @@ async def test_button_app_next( assert device_entry.model_id == "LM 37X8" assert device_entry.name == "Frenck's LaMetric" assert device_entry.sw_version == "2.2.2" + assert device_entry.serial_number == "SA110405124500W00BS9" assert device_entry.hw_version is None await hass.services.async_call( diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index ccbbe005639..c0fb98f1908 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -13,18 +13,18 @@ from demetriek import ( ) import pytest -from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.lametric.const import DOMAIN -from homeassistant.components.ssdp import ( - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_SERIAL, - SsdpServiceInfo, -) from homeassistant.config_entries import SOURCE_DHCP, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py index 681abf850d2..6e052603c24 100644 --- a/tests/components/lametric/test_number.py +++ b/tests/components/lametric/test_number.py @@ -42,7 +42,7 @@ async def test_brightness( assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness" assert state.attributes.get(ATTR_MAX) == 100 - assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_MIN) == 2 assert state.attributes.get(ATTR_STEP) == 1 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "100" @@ -62,6 +62,7 @@ async def test_brightness( assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} assert device.manufacturer == "LaMetric Inc." assert device.name == "Frenck's LaMetric" + assert device.serial_number == "SA110405124500W00BS9" assert device.sw_version == "2.2.2" await hass.services.async_call( @@ -183,3 +184,16 @@ async def test_number_connection_error( state = hass.states.get("number.frenck_s_lametric_volume") assert state assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("device_fixture", ["computer_powered"]) +async def test_computer_powered_devices( + hass: HomeAssistant, + mock_lametric: MagicMock, +) -> None: + """Test Brightness is properly limited for computer powered devices.""" + state = hass.states.get("number.time_brightness") + assert state + assert state.state == "75" + assert state.attributes[ATTR_MIN] == 2 + assert state.attributes[ATTR_MAX] == 76 diff --git a/tests/components/lametric/test_select.py b/tests/components/lametric/test_select.py index 6b3fa291e9c..e4b9870f52b 100644 --- a/tests/components/lametric/test_select.py +++ b/tests/components/lametric/test_select.py @@ -55,6 +55,7 @@ async def test_brightness_mode( assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} assert device.manufacturer == "LaMetric Inc." assert device.name == "Frenck's LaMetric" + assert device.serial_number == "SA110405124500W00BS9" assert device.sw_version == "2.2.2" await hass.services.async_call( diff --git a/tests/components/lametric/test_sensor.py b/tests/components/lametric/test_sensor.py index 8dff11fb450..08b289e2425 100644 --- a/tests/components/lametric/test_sensor.py +++ b/tests/components/lametric/test_sensor.py @@ -48,4 +48,5 @@ async def test_wifi_signal( assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} assert device.manufacturer == "LaMetric Inc." assert device.name == "Frenck's LaMetric" + assert device.serial_number == "SA110405124500W00BS9" assert device.sw_version == "2.2.2" diff --git a/tests/components/lametric/test_switch.py b/tests/components/lametric/test_switch.py index 367d5605e06..3e73b710942 100644 --- a/tests/components/lametric/test_switch.py +++ b/tests/components/lametric/test_switch.py @@ -22,7 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed @@ -57,6 +57,7 @@ async def test_bluetooth( assert device.identifiers == {(DOMAIN, "SA110405124500W00BS9")} assert device.manufacturer == "LaMetric Inc." assert device.name == "Frenck's LaMetric" + assert device.serial_number == "SA110405124500W00BS9" assert device.sw_version == "2.2.2" await hass.services.async_call( diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 0735d4541ff..be588b86e80 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -14,7 +14,7 @@ from homeassistant.components.lawn_mower import ( from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import ( MockConfigEntry, @@ -97,7 +97,7 @@ async def test_lawn_mower_setup(hass: HomeAssistant) -> None: async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test platform via config entry.""" async_add_entities([entity1]) diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index 3c5979c3c36..d8dee472946 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -16,7 +16,6 @@ from homeassistant.components.lcn.helpers import AddressType, generate_unique_id from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_ENTITIES, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @@ -134,15 +133,6 @@ async def init_integration( return lcn_connection -async def setup_component(hass: HomeAssistant) -> None: - """Set up the LCN component.""" - fixture_filename = "lcn/config.json" - config_data = json.loads(load_fixture(fixture_filename)) - - await async_setup_component(hass, DOMAIN, config_data) - await hass.async_block_till_done() - - def get_device( hass: HomeAssistant, entry: MockConfigEntry, address: AddressType ) -> dr.DeviceEntry: diff --git a/tests/components/lcn/fixtures/config.json b/tests/components/lcn/fixtures/config.json deleted file mode 100644 index ed3e3500900..00000000000 --- a/tests/components/lcn/fixtures/config.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "lcn": { - "connections": [ - { - "host": "192.168.2.41", - "port": 4114, - "username": "lcn", - "password": "lcn", - "sk_num_tries": 0, - "dim_mode": "steps200", - "name": "pchk" - }, - { - "name": "myhome", - "host": "192.168.2.42", - "port": 4114, - "username": "lcn", - "password": "lcn", - "sk_num_tries": 0, - "dim_mode": "steps200" - } - ], - "lights": [ - { - "name": "Light_Output1", - "address": "pchk.s0.m7", - "output": "output1", - "dimmable": true, - "transition": 5 - }, - { - "name": "Light_Output2", - "address": "pchk.s0.m7", - "output": "output2", - "dimmable": false, - "transition": 0 - }, - { - "name": "Light_Relay1", - "address": "s0.m7", - "output": "relay1" - }, - { - "name": "Light_Relay3", - "address": "myhome.s0.m7", - "output": "relay3" - }, - { - "name": "Light_Relay4", - "address": "myhome.s0.m7", - "output": "relay4" - } - ], - "switches": [ - { - "name": "Switch_Output1", - "address": "s0.m7", - "output": "output1" - }, - { - "name": "Switch_Output2", - "address": "s0.m7", - "output": "output2" - }, - { - "name": "Switch_Relay1", - "address": "s0.m7", - "output": "relay1" - }, - { - "name": "Switch_Relay2", - "address": "s0.m7", - "output": "relay2" - }, - { - "name": "Switch_Group5", - "address": "s0.g5", - "output": "relay1" - } - ], - "covers": [ - { - "name": "Cover_Ouputs", - "address": "s0.m7", - "motor": "outputs", - "reverse_time": "rt1200" - }, - { - "name": "Cover_Relays", - "address": "s0.m7", - "motor": "motor1" - } - ], - "climates": [ - { - "name": "Climate1", - "address": "s0.m7", - "source": "var1", - "setpoint": "r1varsetpoint", - "lockable": true, - "min_temp": 0, - "max_temp": 40, - "unit_of_measurement": "°C" - } - ], - "scenes": [ - { - "name": "Romantic", - "address": "s0.m7", - "register": 0, - "scene": 0, - "outputs": ["output1", "output2", "relay1"] - }, - { - "name": "Romantic Transition", - "address": "s0.m7", - "register": 0, - "scene": 1, - "outputs": ["output1", "output2", "relay1"], - "transition": 10 - } - ], - "binary_sensors": [ - { - "name": "Sensor_LockRegulator1", - "address": "s0.m7", - "source": "r1varsetpoint" - }, - { - "name": "Binary_Sensor1", - "address": "s0.m7", - "source": "binsensor1" - }, - { - "name": "Sensor_KeyLock", - "address": "s0.m7", - "source": "a5" - } - ], - "sensors": [ - { - "name": "Sensor_Var1", - "address": "s0.m7", - "source": "var1", - "unit_of_measurement": "°C" - }, - { - "name": "Sensor_Setpoint1", - "address": "s0.m7", - "source": "r1varsetpoint", - "unit_of_measurement": "°C" - }, - { - "name": "Sensor_Led6", - "address": "s0.m7", - "source": "led6" - }, - { - "name": "Sensor_LogicOp1", - "address": "s0.m7", - "source": "logicop1" - } - ] - } -} diff --git a/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json b/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json index e1893c30b42..7dea4405fc5 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json +++ b/tests/components/lcn/fixtures/config_entry_pchk_v1_1.json @@ -13,13 +13,6 @@ "hardware_serial": -1, "software_serial": -1, "hardware_type": -1 - }, - { - "address": [0, 5, true], - "name": "TestGroup", - "hardware_serial": -1, - "software_serial": -1, - "hardware_type": -1 } ], "entities": [ @@ -33,216 +26,6 @@ "dimmable": true, "transition": 5000.0 } - }, - { - "address": [0, 7, false], - "name": "Light_Output2", - "resource": "output2", - "domain": "light", - "domain_data": { - "output": "OUTPUT2", - "dimmable": false, - "transition": 0 - } - }, - { - "address": [0, 7, false], - "name": "Light_Relay1", - "resource": "relay1", - "domain": "light", - "domain_data": { - "output": "RELAY1", - "dimmable": false, - "transition": 0.0 - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output1", - "resource": "output1", - "domain": "switch", - "domain_data": { - "output": "OUTPUT1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output2", - "resource": "output2", - "domain": "switch", - "domain_data": { - "output": "OUTPUT2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay1", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay2", - "resource": "relay2", - "domain": "switch", - "domain_data": { - "output": "RELAY2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Regulator1", - "resource": "r1varsetpoint", - "domain": "switch", - "domain_data": { - "output": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Switch_KeyLock1", - "resource": "a1", - "domain": "switch", - "domain_data": { - "output": "A1" - } - }, - { - "address": [0, 5, true], - "name": "Switch_Group5", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Outputs", - "resource": "outputs", - "domain": "cover", - "domain_data": { - "motor": "OUTPUTS", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Relays", - "resource": "motor1", - "domain": "cover", - "domain_data": { - "motor": "MOTOR1", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Climate1", - "resource": "var1.r1varsetpoint", - "domain": "climate", - "domain_data": { - "source": "VAR1", - "setpoint": "R1VARSETPOINT", - "lockable": true, - "min_temp": 0.0, - "max_temp": 40.0, - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Romantic", - "resource": "0.0", - "domain": "scene", - "domain_data": { - "register": 0, - "scene": 0, - "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": null - } - }, - { - "address": [0, 7, false], - "name": "Romantic Transition", - "resource": "0.1", - "domain": "scene", - "domain_data": { - "register": 0, - "scene": 1, - "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], - "transition": 10000 - } - }, - { - "address": [0, 7, false], - "name": "Sensor_LockRegulator1", - "resource": "r1varsetpoint", - "domain": "binary_sensor", - "domain_data": { - "source": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Binary_Sensor1", - "resource": "binsensor1", - "domain": "binary_sensor", - "domain_data": { - "source": "BINSENSOR1" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_KeyLock", - "resource": "a5", - "domain": "binary_sensor", - "domain_data": { - "source": "A5" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Var1", - "resource": "var1", - "domain": "sensor", - "domain_data": { - "source": "VAR1", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Setpoint1", - "resource": "r1varsetpoint", - "domain": "sensor", - "domain_data": { - "source": "R1VARSETPOINT", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Led6", - "resource": "led6", - "domain": "sensor", - "domain_data": { - "source": "LED6", - "unit_of_measurement": "NATIVE" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_LogicOp1", - "resource": "logicop1", - "domain": "sensor", - "domain_data": { - "source": "LOGICOP1", - "unit_of_measurement": "NATIVE" - } } ] } diff --git a/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json b/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json index 7389079dca9..4cade6b64d0 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json +++ b/tests/components/lcn/fixtures/config_entry_pchk_v1_2.json @@ -14,13 +14,6 @@ "hardware_serial": -1, "software_serial": -1, "hardware_type": -1 - }, - { - "address": [0, 5, true], - "name": "TestGroup", - "hardware_serial": -1, - "software_serial": -1, - "hardware_type": -1 } ], "entities": [ @@ -43,115 +36,7 @@ "domain_data": { "output": "OUTPUT2", "dimmable": false, - "transition": 0 - } - }, - { - "address": [0, 7, false], - "name": "Light_Relay1", - "resource": "relay1", - "domain": "light", - "domain_data": { - "output": "RELAY1", - "dimmable": false, - "transition": 0.0 - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output1", - "resource": "output1", - "domain": "switch", - "domain_data": { - "output": "OUTPUT1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Output2", - "resource": "output2", - "domain": "switch", - "domain_data": { - "output": "OUTPUT2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay1", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Relay2", - "resource": "relay2", - "domain": "switch", - "domain_data": { - "output": "RELAY2" - } - }, - { - "address": [0, 7, false], - "name": "Switch_Regulator1", - "resource": "r1varsetpoint", - "domain": "switch", - "domain_data": { - "output": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Switch_KeyLock1", - "resource": "a1", - "domain": "switch", - "domain_data": { - "output": "A1" - } - }, - { - "address": [0, 5, true], - "name": "Switch_Group5", - "resource": "relay1", - "domain": "switch", - "domain_data": { - "output": "RELAY1" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Outputs", - "resource": "outputs", - "domain": "cover", - "domain_data": { - "motor": "OUTPUTS", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Cover_Relays", - "resource": "motor1", - "domain": "cover", - "domain_data": { - "motor": "MOTOR1", - "reverse_time": "RT1200" - } - }, - { - "address": [0, 7, false], - "name": "Climate1", - "resource": "var1.r1varsetpoint", - "domain": "climate", - "domain_data": { - "source": "VAR1", - "setpoint": "R1VARSETPOINT", - "lockable": true, - "min_temp": 0.0, - "max_temp": 40.0, - "unit_of_measurement": "°C" + "transition": null } }, { @@ -177,73 +62,6 @@ "outputs": ["OUTPUT1", "OUTPUT2", "RELAY1"], "transition": 10000 } - }, - { - "address": [0, 7, false], - "name": "Sensor_LockRegulator1", - "resource": "r1varsetpoint", - "domain": "binary_sensor", - "domain_data": { - "source": "R1VARSETPOINT" - } - }, - { - "address": [0, 7, false], - "name": "Binary_Sensor1", - "resource": "binsensor1", - "domain": "binary_sensor", - "domain_data": { - "source": "BINSENSOR1" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_KeyLock", - "resource": "a5", - "domain": "binary_sensor", - "domain_data": { - "source": "A5" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Var1", - "resource": "var1", - "domain": "sensor", - "domain_data": { - "source": "VAR1", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Setpoint1", - "resource": "r1varsetpoint", - "domain": "sensor", - "domain_data": { - "source": "R1VARSETPOINT", - "unit_of_measurement": "°C" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_Led6", - "resource": "led6", - "domain": "sensor", - "domain_data": { - "source": "LED6", - "unit_of_measurement": "NATIVE" - } - }, - { - "address": [0, 7, false], - "name": "Sensor_LogicOp1", - "resource": "logicop1", - "domain": "sensor", - "domain_data": { - "source": "LOGICOP1", - "unit_of_measurement": "NATIVE" - } } ] } diff --git a/tests/components/lcn/snapshots/test_binary_sensor.ambr b/tests/components/lcn/snapshots/test_binary_sensor.ambr index 0ad31437dd1..d2d697569d1 100644 --- a/tests/components/lcn/snapshots/test_binary_sensor.ambr +++ b/tests/components/lcn/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/snapshots/test_climate.ambr b/tests/components/lcn/snapshots/test_climate.ambr index 443b13312d1..81745ca8515 100644 --- a/tests/components/lcn/snapshots/test_climate.ambr +++ b/tests/components/lcn/snapshots/test_climate.ambr @@ -13,6 +13,7 @@ 'min_temp': 0.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/snapshots/test_cover.ambr b/tests/components/lcn/snapshots/test_cover.ambr index 82a19060d73..d399626537d 100644 --- a/tests/components/lcn/snapshots/test_cover.ambr +++ b/tests/components/lcn/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/snapshots/test_init.ambr b/tests/components/lcn/snapshots/test_init.ambr new file mode 100644 index 00000000000..ea6267aaa0b --- /dev/null +++ b/tests/components/lcn/snapshots/test_init.ambr @@ -0,0 +1,140 @@ +# serializer version: 1 +# name: test_migrate_1_1 + dict({ + 'acknowledge': False, + 'devices': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'hardware_serial': -1, + 'hardware_type': -1, + 'name': 'TestModule', + 'software_serial': -1, + }), + ]), + 'dim_mode': 'STEPS200', + 'entities': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'light', + 'domain_data': dict({ + 'dimmable': True, + 'output': 'OUTPUT1', + 'transition': 5.0, + }), + 'name': 'Light_Output1', + 'resource': 'output1', + }), + ]), + 'host': 'pchk', + 'ip_address': '192.168.2.41', + 'password': 'lcn', + 'port': 4114, + 'sk_num_tries': 0, + 'username': 'lcn', + }) +# --- +# name: test_migrate_1_2 + dict({ + 'acknowledge': False, + 'devices': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'hardware_serial': -1, + 'hardware_type': -1, + 'name': 'TestModule', + 'software_serial': -1, + }), + ]), + 'dim_mode': 'STEPS200', + 'entities': list([ + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'light', + 'domain_data': dict({ + 'dimmable': True, + 'output': 'OUTPUT1', + 'transition': 5.0, + }), + 'name': 'Light_Output1', + 'resource': 'output1', + }), + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'light', + 'domain_data': dict({ + 'dimmable': False, + 'output': 'OUTPUT2', + 'transition': 0.0, + }), + 'name': 'Light_Output2', + 'resource': 'output2', + }), + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'scene', + 'domain_data': dict({ + 'outputs': list([ + 'OUTPUT1', + 'OUTPUT2', + 'RELAY1', + ]), + 'register': 0, + 'scene': 0, + 'transition': 0.0, + }), + 'name': 'Romantic', + 'resource': '0.0', + }), + dict({ + 'address': tuple( + 0, + 7, + False, + ), + 'domain': 'scene', + 'domain_data': dict({ + 'outputs': list([ + 'OUTPUT1', + 'OUTPUT2', + 'RELAY1', + ]), + 'register': 0, + 'scene': 1, + 'transition': 10.0, + }), + 'name': 'Romantic Transition', + 'resource': '0.1', + }), + ]), + 'host': 'pchk', + 'ip_address': '192.168.2.41', + 'password': 'lcn', + 'port': 4114, + 'sk_num_tries': 0, + 'username': 'lcn', + }) +# --- diff --git a/tests/components/lcn/snapshots/test_light.ambr b/tests/components/lcn/snapshots/test_light.ambr index f53d1fdf2dc..638cddc15cd 100644 --- a/tests/components/lcn/snapshots/test_light.ambr +++ b/tests/components/lcn/snapshots/test_light.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -121,6 +123,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/snapshots/test_scene.ambr b/tests/components/lcn/snapshots/test_scene.ambr index c039c4ef951..a5576158621 100644 --- a/tests/components/lcn/snapshots/test_scene.ambr +++ b/tests/components/lcn/snapshots/test_scene.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/snapshots/test_sensor.ambr b/tests/components/lcn/snapshots/test_sensor.ambr index 56776e3e0f6..f8d57ed8904 100644 --- a/tests/components/lcn/snapshots/test_sensor.ambr +++ b/tests/components/lcn/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -146,6 +149,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/snapshots/test_switch.ambr b/tests/components/lcn/snapshots/test_switch.ambr index 36145b8d4fd..bc69b0ed483 100644 --- a/tests/components/lcn/snapshots/test_switch.ambr +++ b/tests/components/lcn/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -282,6 +288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lcn/test_binary_sensor.py b/tests/components/lcn/test_binary_sensor.py index 2f64f421b93..7d636f546c4 100644 --- a/tests/components/lcn/test_binary_sensor.py +++ b/tests/components/lcn/test_binary_sensor.py @@ -15,8 +15,7 @@ from homeassistant.components.lcn.helpers import get_device_connection from homeassistant.components.script import scripts_with_entity from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.issue_registry as ir +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component from .conftest import MockConfigEntry, init_integration diff --git a/tests/components/lcn/test_climate.py b/tests/components/lcn/test_climate.py index 7ba263bd597..7bac7cc9e81 100644 --- a/tests/components/lcn/test_climate.py +++ b/tests/components/lcn/test_climate.py @@ -107,7 +107,7 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> blocking=True, ) - lock_regulator.assert_awaited_with(0, True) + lock_regulator.assert_awaited_with(0, True, -1) state = hass.states.get("climate.climate1") assert state is not None @@ -124,7 +124,7 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> blocking=True, ) - lock_regulator.assert_awaited_with(0, True) + lock_regulator.assert_awaited_with(0, True, -1) state = hass.states.get("climate.climate1") assert state is not None diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index b7967c247ec..478f2c0949e 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -2,7 +2,12 @@ from unittest.mock import patch -from pypck.connection import PchkAuthenticationError, PchkLicenseError +from pypck.connection import ( + PchkAuthenticationError, + PchkConnectionFailedError, + PchkConnectionRefusedError, + PchkLicenseError, +) import pytest from homeassistant import config_entries, data_entry_flow @@ -98,7 +103,8 @@ async def test_step_user_existing_host( [ (PchkAuthenticationError, {CONF_BASE: "authentication_error"}), (PchkLicenseError, {CONF_BASE: "license_error"}), - (TimeoutError, {CONF_BASE: "connection_refused"}), + (PchkConnectionFailedError, {CONF_BASE: "connection_refused"}), + (PchkConnectionRefusedError, {CONF_BASE: "connection_refused"}), ], ) async def test_step_user_error( @@ -149,7 +155,8 @@ async def test_step_reconfigure(hass: HomeAssistant, entry: MockConfigEntry) -> [ (PchkAuthenticationError, {CONF_BASE: "authentication_error"}), (PchkLicenseError, {CONF_BASE: "license_error"}), - (TimeoutError, {CONF_BASE: "connection_refused"}), + (PchkConnectionFailedError, {CONF_BASE: "connection_refused"}), + (PchkConnectionRefusedError, {CONF_BASE: "connection_refused"}), ], ) async def test_step_reconfigure_error( diff --git a/tests/components/lcn/test_events.py b/tests/components/lcn/test_events.py index c6c3559e821..93998e24a89 100644 --- a/tests/components/lcn/test_events.py +++ b/tests/components/lcn/test_events.py @@ -150,19 +150,3 @@ async def test_dont_fire_on_non_module_input( await lcn_connection.async_process_input(inp) await hass.async_block_till_done() assert len(events) == 0 - - -async def test_dont_fire_on_unknown_module( - hass: HomeAssistant, entry: MockConfigEntry -) -> None: - """Test for no event is fired if an input from an unknown module is received.""" - lcn_connection = await init_integration(hass, entry) - inp = ModStatusAccessControl( - LcnAddr(0, 10, False), # unknown module - periphery=AccessControlPeriphery.FINGERPRINT, - code="aabbcc", - ) - events = async_capture_events(hass, LCN_FINGERPRINT) - await lcn_connection.async_process_input(inp) - await hass.async_block_till_done() - assert len(events) == 0 diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index 2327635e356..ef3c2d3cb66 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -2,8 +2,16 @@ from unittest.mock import Mock, patch -from pypck.connection import PchkAuthenticationError, PchkLicenseError +from pypck.connection import ( + PchkAuthenticationError, + PchkConnectionFailedError, + PchkConnectionRefusedError, + PchkLcnNotConnectedError, + PchkLicenseError, +) +from pypck.lcn_defs import LcnEvent import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.lcn.const import DOMAIN @@ -84,25 +92,50 @@ async def test_async_setup_entry_update( @pytest.mark.parametrize( - "exception", [PchkAuthenticationError, PchkLicenseError, TimeoutError] + "exception", + [ + PchkAuthenticationError, + PchkLicenseError, + PchkConnectionRefusedError, + PchkConnectionFailedError, + PchkLcnNotConnectedError, + ], ) -async def test_async_setup_entry_raises_authentication_error( +async def test_async_setup_entry_fails( hass: HomeAssistant, entry: MockConfigEntry, exception: Exception ) -> None: - """Test that an authentication error is handled properly.""" - with patch( - "homeassistant.components.lcn.PchkConnectionManager.async_connect", - side_effect=exception, + """Test that an error is handled properly.""" + with ( + patch( + "homeassistant.components.lcn.PchkConnectionManager.async_connect", + side_effect=exception, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + "event", + [LcnEvent.CONNECTION_LOST, LcnEvent.PING_TIMEOUT, LcnEvent.BUS_DISCONNECTED], +) +async def test_async_entry_reload_on_host_event_received( + hass: HomeAssistant, entry: MockConfigEntry, event: LcnEvent +) -> None: + """Test for config entry reload on certain host event received.""" + lcn_connection = await init_integration(hass, entry) + with patch( + "homeassistant.config_entries.ConfigEntries.async_schedule_reload" + ) as async_schedule_reload: + lcn_connection.fire_event(event) + async_schedule_reload.assert_called_with(entry.entry_id) @patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_migrate_1_1(hass: HomeAssistant, entry) -> None: +async def test_migrate_1_1(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: """Test migration config entry.""" entry_v1_1 = create_config_entry("pchk_v1_1", version=(1, 1)) entry_v1_1.add_to_hass(hass) @@ -111,14 +144,15 @@ async def test_migrate_1_1(hass: HomeAssistant, entry) -> None: await hass.async_block_till_done() entry_migrated = hass.config_entries.async_get_entry(entry_v1_1.entry_id) + assert entry_migrated.state is ConfigEntryState.LOADED assert entry_migrated.version == 2 assert entry_migrated.minor_version == 1 - assert entry_migrated.data == entry.data + assert entry_migrated.data == snapshot @patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_migrate_1_2(hass: HomeAssistant, entry) -> None: +async def test_migrate_1_2(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None: """Test migration config entry.""" entry_v1_2 = create_config_entry("pchk_v1_2", version=(1, 2)) entry_v1_2.add_to_hass(hass) @@ -127,7 +161,8 @@ async def test_migrate_1_2(hass: HomeAssistant, entry) -> None: await hass.async_block_till_done() entry_migrated = hass.config_entries.async_get_entry(entry_v1_2.entry_id) + assert entry_migrated.state is ConfigEntryState.LOADED assert entry_migrated.version == 2 assert entry_migrated.minor_version == 1 - assert entry_migrated.data == entry.data + assert entry_migrated.data == snapshot diff --git a/tests/components/lcn/test_services.py b/tests/components/lcn/test_services.py index a4ea559cd72..c9eda40fdba 100644 --- a/tests/components/lcn/test_services.py +++ b/tests/components/lcn/test_services.py @@ -26,22 +26,37 @@ from homeassistant.components.lcn.services import LcnService from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, + CONF_DEVICE_ID, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from .conftest import ( MockConfigEntry, MockModuleConnection, - MockPchkConnectionManager, + get_device, init_integration, ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_output_abs(hass: HomeAssistant, entry: MockConfigEntry) -> None: +def device_config( + hass: HomeAssistant, entry: MockConfigEntry, config_type: str +) -> dict[str, str]: + """Return test device config depending on type.""" + if config_type == CONF_ADDRESS: + return {CONF_ADDRESS: "pchk.s0.m7"} + return {CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id} + + +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_output_abs( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test output_abs service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -51,7 +66,7 @@ async def test_service_output_abs(hass: HomeAssistant, entry: MockConfigEntry) - DOMAIN, LcnService.OUTPUT_ABS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_OUTPUT: "output1", CONF_BRIGHTNESS: 100, CONF_TRANSITION: 5, @@ -62,8 +77,12 @@ async def test_service_output_abs(hass: HomeAssistant, entry: MockConfigEntry) - dim_output.assert_awaited_with(0, 100, 9) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_output_rel(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_output_rel( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test output_rel service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -73,7 +92,7 @@ async def test_service_output_rel(hass: HomeAssistant, entry: MockConfigEntry) - DOMAIN, LcnService.OUTPUT_REL, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_OUTPUT: "output1", CONF_BRIGHTNESS: 25, }, @@ -83,9 +102,11 @@ async def test_service_output_rel(hass: HomeAssistant, entry: MockConfigEntry) - rel_output.assert_awaited_with(0, 25) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_output_toggle( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test output_toggle service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -96,7 +117,7 @@ async def test_service_output_toggle( DOMAIN, LcnService.OUTPUT_TOGGLE, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_OUTPUT: "output1", CONF_TRANSITION: 5, }, @@ -106,8 +127,12 @@ async def test_service_output_toggle( toggle_output.assert_awaited_with(0, 9) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_relays(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_relays( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test relays service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -116,7 +141,7 @@ async def test_service_relays(hass: HomeAssistant, entry: MockConfigEntry) -> No await hass.services.async_call( DOMAIN, LcnService.RELAYS, - {CONF_ADDRESS: "pchk.s0.m7", CONF_STATE: "0011TT--"}, + {**device_config(hass, entry, config_type), CONF_STATE: "0011TT--"}, blocking=True, ) @@ -126,8 +151,12 @@ async def test_service_relays(hass: HomeAssistant, entry: MockConfigEntry) -> No control_relays.assert_awaited_with(relay_states) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_led(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_led( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test led service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -136,7 +165,11 @@ async def test_service_led(hass: HomeAssistant, entry: MockConfigEntry) -> None: await hass.services.async_call( DOMAIN, LcnService.LED, - {CONF_ADDRESS: "pchk.s0.m7", CONF_LED: "led6", CONF_STATE: "blink"}, + { + **device_config(hass, entry, config_type), + CONF_LED: "led6", + CONF_STATE: "blink", + }, blocking=True, ) @@ -146,8 +179,12 @@ async def test_service_led(hass: HomeAssistant, entry: MockConfigEntry) -> None: control_led.assert_awaited_with(led, led_state) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_var_abs(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_var_abs( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test var_abs service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -157,7 +194,7 @@ async def test_service_var_abs(hass: HomeAssistant, entry: MockConfigEntry) -> N DOMAIN, LcnService.VAR_ABS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_VARIABLE: "var1", CONF_VALUE: 75, CONF_UNIT_OF_MEASUREMENT: "%", @@ -170,8 +207,12 @@ async def test_service_var_abs(hass: HomeAssistant, entry: MockConfigEntry) -> N ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_var_rel(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_var_rel( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test var_rel service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -181,7 +222,7 @@ async def test_service_var_rel(hass: HomeAssistant, entry: MockConfigEntry) -> N DOMAIN, LcnService.VAR_REL, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_VARIABLE: "var1", CONF_VALUE: 10, CONF_UNIT_OF_MEASUREMENT: "%", @@ -198,8 +239,12 @@ async def test_service_var_rel(hass: HomeAssistant, entry: MockConfigEntry) -> N ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_var_reset(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_var_reset( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test var_reset service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -208,16 +253,18 @@ async def test_service_var_reset(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.VAR_RESET, - {CONF_ADDRESS: "pchk.s0.m7", CONF_VARIABLE: "var1"}, + {**device_config(hass, entry, config_type), CONF_VARIABLE: "var1"}, blocking=True, ) var_reset.assert_awaited_with(pypck.lcn_defs.Var["VAR1"]) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_regulator( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test lock_regulator service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -228,7 +275,7 @@ async def test_service_lock_regulator( DOMAIN, LcnService.LOCK_REGULATOR, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_SETPOINT: "r1varsetpoint", CONF_STATE: True, }, @@ -238,8 +285,12 @@ async def test_service_lock_regulator( lock_regulator.assert_awaited_with(0, True) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_send_keys(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_send_keys( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test send_keys service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -248,7 +299,11 @@ async def test_service_send_keys(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.SEND_KEYS, - {CONF_ADDRESS: "pchk.s0.m7", CONF_KEYS: "a1a5d8", CONF_STATE: "hit"}, + { + **device_config(hass, entry, config_type), + CONF_KEYS: "a1a5d8", + CONF_STATE: "hit", + }, blocking=True, ) @@ -260,9 +315,11 @@ async def test_service_send_keys(hass: HomeAssistant, entry: MockConfigEntry) -> send_keys.assert_awaited_with(keys, pypck.lcn_defs.SendKeyCommand["HIT"]) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_send_keys_hit_deferred( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test send_keys (hit_deferred) service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -281,7 +338,7 @@ async def test_service_send_keys_hit_deferred( DOMAIN, LcnService.SEND_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_KEYS: "a1a5d8", CONF_TIME: 5, CONF_TIME_UNIT: "s", @@ -304,7 +361,7 @@ async def test_service_send_keys_hit_deferred( DOMAIN, LcnService.SEND_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_KEYS: "a1a5d8", CONF_STATE: "make", CONF_TIME: 5, @@ -314,8 +371,12 @@ async def test_service_send_keys_hit_deferred( ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_lock_keys(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_lock_keys( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test lock_keys service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -324,7 +385,11 @@ async def test_service_lock_keys(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.LOCK_KEYS, - {CONF_ADDRESS: "pchk.s0.m7", CONF_TABLE: "a", CONF_STATE: "0011TT--"}, + { + **device_config(hass, entry, config_type), + CONF_TABLE: "a", + CONF_STATE: "0011TT--", + }, blocking=True, ) @@ -334,9 +399,11 @@ async def test_service_lock_keys(hass: HomeAssistant, entry: MockConfigEntry) -> lock_keys.assert_awaited_with(0, lock_states) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_keys_tab_a_temporary( - hass: HomeAssistant, entry: MockConfigEntry + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, ) -> None: """Test lock_keys (tab_a_temporary) service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -350,7 +417,7 @@ async def test_service_lock_keys_tab_a_temporary( DOMAIN, LcnService.LOCK_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_STATE: "0011TT--", CONF_TIME: 10, CONF_TIME_UNIT: "s", @@ -376,7 +443,7 @@ async def test_service_lock_keys_tab_a_temporary( DOMAIN, LcnService.LOCK_KEYS, { - CONF_ADDRESS: "pchk.s0.m7", + **device_config(hass, entry, config_type), CONF_TABLE: "b", CONF_STATE: "0011TT--", CONF_TIME: 10, @@ -386,8 +453,12 @@ async def test_service_lock_keys_tab_a_temporary( ) -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_dyn_text(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_dyn_text( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test dyn_text service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -396,15 +467,23 @@ async def test_service_dyn_text(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN, LcnService.DYN_TEXT, - {CONF_ADDRESS: "pchk.s0.m7", CONF_ROW: 1, CONF_TEXT: "text in row 1"}, + { + **device_config(hass, entry, config_type), + CONF_ROW: 1, + CONF_TEXT: "text in row 1", + }, blocking=True, ) dyn_text.assert_awaited_with(0, "text in row 1") -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) -async def test_service_pck(hass: HomeAssistant, entry: MockConfigEntry) -> None: +@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) +async def test_service_pck( + hass: HomeAssistant, + entry: MockConfigEntry, + config_type: str, +) -> None: """Test pck service.""" await async_setup_component(hass, "persistent_notification", {}) await init_integration(hass, entry) @@ -413,14 +492,13 @@ async def test_service_pck(hass: HomeAssistant, entry: MockConfigEntry) -> None: await hass.services.async_call( DOMAIN, LcnService.PCK, - {CONF_ADDRESS: "pchk.s0.m7", CONF_PCK: "PIN4"}, + {**device_config(hass, entry, config_type), CONF_PCK: "PIN4"}, blocking=True, ) pck.assert_awaited_with("PIN4") -@patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) async def test_service_called_with_invalid_host_id( hass: HomeAssistant, entry: MockConfigEntry ) -> None: @@ -437,3 +515,20 @@ async def test_service_called_with_invalid_host_id( ) pck.assert_not_awaited() + + +async def test_service_with_deprecated_address_parameter( + hass: HomeAssistant, entry: MockConfigEntry, issue_registry: ir.IssueRegistry +) -> None: + """Test service puts issue in registry if called with address parameter.""" + await async_setup_component(hass, "persistent_notification", {}) + await init_integration(hass, entry) + + await hass.services.async_call( + DOMAIN, + LcnService.PCK, + {CONF_ADDRESS: "pchk.s0.m7", CONF_PCK: "PIN4"}, + blocking=True, + ) + + assert issue_registry.async_get_issue(DOMAIN, "deprecated_address_parameter") diff --git a/tests/components/led_ble/test_config_flow.py b/tests/components/led_ble/test_config_flow.py index c22c62e2fb1..674700aebd9 100644 --- a/tests/components/led_ble/test_config_flow.py +++ b/tests/components/led_ble/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import patch from bleak import BleakError +from led_ble import CharacteristicMissingError from homeassistant import config_entries from homeassistant.components.led_ble.const import DOMAIN @@ -202,6 +203,35 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_step_not_supported(hass: HomeAssistant) -> None: + """Test user step with a non supported device.""" + with patch( + "homeassistant.components.led_ble.config_flow.async_discovered_service_info", + return_value=[LED_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + side_effect=CharacteristicMissingError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LED_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "not_supported" + + async def test_bluetooth_step_success(hass: HomeAssistant) -> None: """Test bluetooth step success path.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/lektrico/conftest.py b/tests/components/lektrico/conftest.py index fd840b0c290..0b120cd6e23 100644 --- a/tests/components/lektrico/conftest.py +++ b/tests/components/lektrico/conftest.py @@ -8,13 +8,13 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.lektrico.const import DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import ( ATTR_HW_VERSION, ATTR_SERIAL_NUMBER, CONF_HOST, CONF_TYPE, ) +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/lektrico/snapshots/test_binary_sensor.ambr b/tests/components/lektrico/snapshots/test_binary_sensor.ambr index 6a28e7c60de..b365ff84187 100644 --- a/tests/components/lektrico/snapshots/test_binary_sensor.ambr +++ b/tests/components/lektrico/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -382,6 +390,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -429,6 +438,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lektrico/snapshots/test_button.ambr b/tests/components/lektrico/snapshots/test_button.ambr index 5070cd484c4..f9cb7189237 100644 --- a/tests/components/lektrico/snapshots/test_button.ambr +++ b/tests/components/lektrico/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lektrico/snapshots/test_init.ambr b/tests/components/lektrico/snapshots/test_init.ambr index 63739e1c9d8..35183bf5d75 100644 --- a/tests/components/lektrico/snapshots/test_init.ambr +++ b/tests/components/lektrico/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/lektrico/snapshots/test_number.ambr b/tests/components/lektrico/snapshots/test_number.ambr index 30a37a25a09..57cf40567e7 100644 --- a/tests/components/lektrico/snapshots/test_number.ambr +++ b/tests/components/lektrico/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lektrico/snapshots/test_select.ambr b/tests/components/lektrico/snapshots/test_select.ambr index 5a964f52ada..0f564abb146 100644 --- a/tests/components/lektrico/snapshots/test_select.ambr +++ b/tests/components/lektrico/snapshots/test_select.ambr @@ -13,6 +13,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lektrico/snapshots/test_sensor.ambr b/tests/components/lektrico/snapshots/test_sensor.ambr index 73ec88e6fa1..aa146f55776 100644 --- a/tests/components/lektrico/snapshots/test_sensor.ambr +++ b/tests/components/lektrico/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -105,6 +107,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -153,6 +156,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -203,6 +207,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -266,6 +271,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -328,6 +334,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -392,6 +399,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -452,6 +460,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -501,6 +510,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lektrico/snapshots/test_switch.ambr b/tests/components/lektrico/snapshots/test_switch.ambr index 3f4a1693315..c55e96ac9a9 100644 --- a/tests/components/lektrico/snapshots/test_switch.ambr +++ b/tests/components/lektrico/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/letpot/__init__.py b/tests/components/letpot/__init__.py new file mode 100644 index 00000000000..6e73bb430cf --- /dev/null +++ b/tests/components/letpot/__init__.py @@ -0,0 +1,67 @@ +"""Tests for the LetPot integration.""" + +import datetime + +from letpot.models import ( + AuthenticationInfo, + LetPotDeviceErrors, + LetPotDeviceStatus, + TemperatureUnit, +) + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +AUTHENTICATION = AuthenticationInfo( + access_token="access_token", + access_token_expires=1738368000, # 2025-02-01 00:00:00 GMT + refresh_token="refresh_token", + refresh_token_expires=1740441600, # 2025-02-25 00:00:00 GMT + user_id="a1b2c3d4e5f6a1b2c3d4e5f6", + email="email@example.com", +) + +MAX_STATUS = LetPotDeviceStatus( + errors=LetPotDeviceErrors(low_water=True, low_nutrients=False, refill_error=False), + light_brightness=500, + light_mode=1, + light_schedule_end=datetime.time(18, 0), + light_schedule_start=datetime.time(8, 0), + online=True, + plant_days=1, + pump_mode=1, + pump_nutrient=None, + pump_status=0, + raw=[], # Not used by integration, and it requires a real device to get + system_on=True, + system_sound=False, + temperature_unit=TemperatureUnit.CELSIUS, + temperature_value=18, + water_mode=1, + water_level=100, +) + +SE_STATUS = LetPotDeviceStatus( + errors=LetPotDeviceErrors(low_water=True, pump_malfunction=True), + light_brightness=500, + light_mode=1, + light_schedule_end=datetime.time(18, 0), + light_schedule_start=datetime.time(8, 0), + online=True, + plant_days=1, + pump_mode=1, + pump_nutrient=None, + pump_status=0, + raw=[], # Not used by integration, and it requires a real device to get + system_on=True, + system_sound=False, +) diff --git a/tests/components/letpot/conftest.py b/tests/components/letpot/conftest.py new file mode 100644 index 00000000000..25974b2d78a --- /dev/null +++ b/tests/components/letpot/conftest.py @@ -0,0 +1,138 @@ +"""Common fixtures for the LetPot tests.""" + +from collections.abc import Callable, Generator +from unittest.mock import AsyncMock, patch + +from letpot.models import DeviceFeature, LetPotDevice, LetPotDeviceStatus +import pytest + +from homeassistant.components.letpot.const import ( + CONF_ACCESS_TOKEN_EXPIRES, + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_EXPIRES, + CONF_USER_ID, + DOMAIN, +) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL + +from . import AUTHENTICATION, MAX_STATUS, SE_STATUS + +from tests.common import MockConfigEntry + + +@pytest.fixture +def device_type() -> str: + """Return the device type to use for mock data.""" + return "LPH63" + + +def _mock_device_features(device_type: str) -> DeviceFeature: + """Return mock device feature support for the given type.""" + if device_type == "LPH31": + return DeviceFeature.LIGHT_BRIGHTNESS_LOW_HIGH | DeviceFeature.PUMP_STATUS + if device_type == "LPH63": + return ( + DeviceFeature.LIGHT_BRIGHTNESS_LEVELS + | DeviceFeature.NUTRIENT_BUTTON + | DeviceFeature.PUMP_AUTO + | DeviceFeature.PUMP_STATUS + | DeviceFeature.TEMPERATURE + | DeviceFeature.WATER_LEVEL + ) + raise ValueError(f"No mock data for device type {device_type}") + + +def _mock_device_status(device_type: str) -> LetPotDeviceStatus: + """Return mock device status for the given type.""" + if device_type == "LPH31": + return SE_STATUS + if device_type == "LPH63": + return MAX_STATUS + raise ValueError(f"No mock data for device type {device_type}") + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.letpot.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_client(device_type: str) -> Generator[AsyncMock]: + """Mock a LetPotClient.""" + with ( + patch( + "homeassistant.components.letpot.LetPotClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.letpot.config_flow.LetPotClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.login.return_value = AUTHENTICATION + client.refresh_token.return_value = AUTHENTICATION + client.get_devices.return_value = [ + LetPotDevice( + serial_number=f"{device_type}ABCD", + name="Garden", + device_type=device_type, + is_online=True, + is_remote=False, + ) + ] + yield client + + +@pytest.fixture +def mock_device_client(device_type: str) -> Generator[AsyncMock]: + """Mock a LetPotDeviceClient.""" + with patch( + "homeassistant.components.letpot.coordinator.LetPotDeviceClient", + autospec=True, + ) as mock_device_client: + device_client = mock_device_client.return_value + device_client.device_features = _mock_device_features(device_type) + device_client.device_model_code = device_type + device_client.device_model_name = f"LetPot {device_type}" + device_status = _mock_device_status(device_type) + + subscribe_callbacks: list[Callable] = [] + + def subscribe_side_effect(callback: Callable) -> None: + subscribe_callbacks.append(callback) + + def status_side_effect() -> None: + # Deliver a status update to any subscribers, like the real client + for callback in subscribe_callbacks: + callback(device_status) + + device_client.get_current_status.side_effect = status_side_effect + device_client.get_current_status.return_value = device_status + device_client.last_status.return_value = device_status + device_client.request_status_update.side_effect = status_side_effect + device_client.subscribe.side_effect = subscribe_side_effect + + yield device_client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title=AUTHENTICATION.email, + data={ + CONF_ACCESS_TOKEN: AUTHENTICATION.access_token, + CONF_ACCESS_TOKEN_EXPIRES: AUTHENTICATION.access_token_expires, + CONF_REFRESH_TOKEN: AUTHENTICATION.refresh_token, + CONF_REFRESH_TOKEN_EXPIRES: AUTHENTICATION.refresh_token_expires, + CONF_USER_ID: AUTHENTICATION.user_id, + CONF_EMAIL: AUTHENTICATION.email, + }, + unique_id=AUTHENTICATION.user_id, + ) diff --git a/tests/components/letpot/snapshots/test_binary_sensor.ambr b/tests/components/letpot/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..121cf4e3f82 --- /dev/null +++ b/tests/components/letpot/snapshots/test_binary_sensor.ambr @@ -0,0 +1,337 @@ +# serializer version: 1 +# name: test_all_entities[LPH31][binary_sensor.garden_low_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garden_low_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Low water', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'low_water', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_low_water', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH31][binary_sensor.garden_low_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Garden Low water', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_low_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[LPH31][binary_sensor.garden_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.garden_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_pump', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH31][binary_sensor.garden_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Garden Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[LPH31][binary_sensor.garden_pump_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garden_pump_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump error', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_error', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_pump_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH31][binary_sensor.garden_pump_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Garden Pump error', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_pump_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_low_nutrients-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garden_low_nutrients', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Low nutrients', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'low_nutrients', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_low_nutrients', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_low_nutrients-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Garden Low nutrients', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_low_nutrients', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_low_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garden_low_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Low water', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'low_water', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_low_water', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_low_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Garden Low water', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_low_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.garden_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pump', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_pump', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Garden Pump', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_refill_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.garden_refill_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Refill error', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'refill_error', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_refill_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[LPH63][binary_sensor.garden_refill_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Garden Refill error', + }), + 'context': , + 'entity_id': 'binary_sensor.garden_refill_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/letpot/snapshots/test_sensor.ambr b/tests/components/letpot/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5d123cf6ce0 --- /dev/null +++ b/tests/components/letpot/snapshots/test_sensor.ambr @@ -0,0 +1,104 @@ +# serializer version: 1 +# name: test_all_entities[sensor.garden_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garden_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.garden_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Garden Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.garden_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18', + }) +# --- +# name: test_all_entities[sensor.garden_water_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.garden_water_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water level', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_level', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_water_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.garden_water_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Water level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.garden_water_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- diff --git a/tests/components/letpot/snapshots/test_switch.ambr b/tests/components/letpot/snapshots/test_switch.ambr new file mode 100644 index 00000000000..1a36e555dd1 --- /dev/null +++ b/tests/components/letpot/snapshots/test_switch.ambr @@ -0,0 +1,189 @@ +# serializer version: 1 +# name: test_all_entities[switch.garden_alarm_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_alarm_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm sound', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_sound', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_alarm_sound', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_alarm_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Alarm sound', + }), + 'context': , + 'entity_id': 'switch.garden_alarm_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[switch.garden_auto_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_auto_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto mode', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_mode', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_auto_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_auto_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Auto mode', + }), + 'context': , + 'entity_id': 'switch.garden_auto_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[switch.garden_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Power', + }), + 'context': , + 'entity_id': 'switch.garden_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[switch.garden_pump_cycling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.garden_pump_cycling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pump cycling', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_cycling', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_pump_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.garden_pump_cycling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Pump cycling', + }), + 'context': , + 'entity_id': 'switch.garden_pump_cycling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/letpot/snapshots/test_time.ambr b/tests/components/letpot/snapshots/test_time.ambr new file mode 100644 index 00000000000..9ca75003e56 --- /dev/null +++ b/tests/components/letpot/snapshots/test_time.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_all_entities[time.garden_light_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'time', + 'entity_category': , + 'entity_id': 'time.garden_light_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light off', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_schedule_end', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_light_schedule_end', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[time.garden_light_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light off', + }), + 'context': , + 'entity_id': 'time.garden_light_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '18:00:00', + }) +# --- +# name: test_all_entities[time.garden_light_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'time', + 'entity_category': , + 'entity_id': 'time.garden_light_on', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light on', + 'platform': 'letpot', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light_schedule_start', + 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_light_schedule_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[time.garden_light_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Light on', + }), + 'context': , + 'entity_id': 'time.garden_light_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '08:00:00', + }) +# --- diff --git a/tests/components/letpot/test_binary_sensor.py b/tests/components/letpot/test_binary_sensor.py new file mode 100644 index 00000000000..03ce1bee1a5 --- /dev/null +++ b/tests/components/letpot/test_binary_sensor.py @@ -0,0 +1,32 @@ +"""Test binary sensor entities for the LetPot integration.""" + +from unittest.mock import MagicMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("device_type", ["LPH63", "LPH31"]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_type: str, +) -> None: + """Test binary sensor entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/letpot/test_config_flow.py b/tests/components/letpot/test_config_flow.py new file mode 100644 index 00000000000..a659b235213 --- /dev/null +++ b/tests/components/letpot/test_config_flow.py @@ -0,0 +1,299 @@ +"""Test the LetPot config flow.""" + +import dataclasses +from typing import Any +from unittest.mock import AsyncMock + +from letpot.exceptions import LetPotAuthenticationException, LetPotConnectionException +import pytest + +from homeassistant.components.letpot.const import ( + CONF_ACCESS_TOKEN_EXPIRES, + CONF_REFRESH_TOKEN, + CONF_REFRESH_TOKEN_EXPIRES, + CONF_USER_ID, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import AUTHENTICATION + +from tests.common import MockConfigEntry + + +def _assert_result_success(result: Any) -> None: + """Assert successful end of flow result, creating an entry.""" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == AUTHENTICATION.email + assert result["data"] == { + CONF_ACCESS_TOKEN: AUTHENTICATION.access_token, + CONF_ACCESS_TOKEN_EXPIRES: AUTHENTICATION.access_token_expires, + CONF_REFRESH_TOKEN: AUTHENTICATION.refresh_token, + CONF_REFRESH_TOKEN_EXPIRES: AUTHENTICATION.refresh_token_expires, + CONF_USER_ID: AUTHENTICATION.user_id, + CONF_EMAIL: AUTHENTICATION.email, + } + assert result["result"].unique_id == AUTHENTICATION.user_id + + +async def test_full_flow( + hass: HomeAssistant, mock_client: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test full flow with success.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "email@example.com", + CONF_PASSWORD: "test-password", + }, + ) + + _assert_result_success(result) + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (LetPotAuthenticationException, "invalid_auth"), + (LetPotConnectionException, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_flow_exceptions( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow with exception during login and recovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_client.login.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "email@example.com", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + # Retry to show recovery. + mock_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "email@example.com", + CONF_PASSWORD: "test-password", + }, + ) + + _assert_result_success(result) + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_duplicate( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test flow aborts when trying to add a previously added account.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "email@example.com", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with success.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + updated_auth = dataclasses.replace( + AUTHENTICATION, + access_token="new_access_token", + refresh_token="new_refresh_token", + ) + mock_client.login.return_value = updated_auth + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data == { + CONF_ACCESS_TOKEN: "new_access_token", + CONF_ACCESS_TOKEN_EXPIRES: AUTHENTICATION.access_token_expires, + CONF_REFRESH_TOKEN: "new_refresh_token", + CONF_REFRESH_TOKEN_EXPIRES: AUTHENTICATION.refresh_token_expires, + CONF_USER_ID: AUTHENTICATION.user_id, + CONF_EMAIL: AUTHENTICATION.email, + } + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (LetPotAuthenticationException, "invalid_auth"), + (LetPotConnectionException, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_reauth_exceptions( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test reauth flow with exception during login and recovery.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_client.login.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + # Retry to show recovery. + updated_auth = dataclasses.replace( + AUTHENTICATION, + access_token="new_access_token", + refresh_token="new_refresh_token", + ) + mock_client.login.return_value = updated_auth + mock_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data == { + CONF_ACCESS_TOKEN: "new_access_token", + CONF_ACCESS_TOKEN_EXPIRES: AUTHENTICATION.access_token_expires, + CONF_REFRESH_TOKEN: "new_refresh_token", + CONF_REFRESH_TOKEN_EXPIRES: AUTHENTICATION.refresh_token_expires, + CONF_USER_ID: AUTHENTICATION.user_id, + CONF_EMAIL: AUTHENTICATION.email, + } + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_reauth_different_user_id_new( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with different, new user ID updating the existing entry.""" + mock_config_entry.add_to_hass(hass) + config_entries = hass.config_entries.async_entries() + assert len(config_entries) == 1 + assert config_entries[0].unique_id == AUTHENTICATION.user_id + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + updated_auth = dataclasses.replace(AUTHENTICATION, user_id="new_user_id") + mock_client.login.return_value = updated_auth + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data == { + CONF_ACCESS_TOKEN: AUTHENTICATION.access_token, + CONF_ACCESS_TOKEN_EXPIRES: AUTHENTICATION.access_token_expires, + CONF_REFRESH_TOKEN: AUTHENTICATION.refresh_token, + CONF_REFRESH_TOKEN_EXPIRES: AUTHENTICATION.refresh_token_expires, + CONF_USER_ID: "new_user_id", + CONF_EMAIL: AUTHENTICATION.email, + } + config_entries = hass.config_entries.async_entries() + assert len(config_entries) == 1 + assert config_entries[0].unique_id == "new_user_id" + + +async def test_reauth_different_user_id_existing( + hass: HomeAssistant, + mock_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow with different, existing user ID aborting.""" + mock_config_entry.add_to_hass(hass) + mock_other = MockConfigEntry( + domain=DOMAIN, title="email2@example.com", data={}, unique_id="other_user_id" + ) + mock_other.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + updated_auth = dataclasses.replace(AUTHENTICATION, user_id="other_user_id") + mock_client.login.return_value = updated_auth + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(hass.config_entries.async_entries()) == 2 diff --git a/tests/components/letpot/test_init.py b/tests/components/letpot/test_init.py new file mode 100644 index 00000000000..e3f78d87dc1 --- /dev/null +++ b/tests/components/letpot/test_init.py @@ -0,0 +1,131 @@ +"""Test the LetPot integration initialization and setup.""" + +from unittest.mock import MagicMock + +from letpot.exceptions import ( + LetPotAuthenticationException, + LetPotConnectionException, + LetPotException, +) +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2025-01-31 00:00:00") +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, +) -> None: + """Test config entry loading/unloading.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + mock_client.refresh_token.assert_not_called() # Didn't refresh valid token + mock_client.get_devices.assert_called_once() + mock_device_client.subscribe.assert_called_once() + mock_device_client.get_current_status.assert_called_once() + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + mock_device_client.disconnect.assert_called_once() + + +@pytest.mark.freeze_time("2025-02-15 00:00:00") +async def test_refresh_authentication_on_load( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, +) -> None: + """Test expired access token refreshed when needed to load config entry.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + mock_client.refresh_token.assert_called_once() + + # Check loading continued as expected after refreshing token + mock_client.get_devices.assert_called_once() + mock_device_client.subscribe.assert_called_once() + mock_device_client.get_current_status.assert_called_once() + + +@pytest.mark.freeze_time("2025-03-01 00:00:00") +async def test_refresh_token_error_aborts( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test expired refresh token aborting config entry loading.""" + mock_client.refresh_token.side_effect = LetPotAuthenticationException + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + mock_client.refresh_token.assert_called_once() + mock_client.get_devices.assert_not_called() + + +@pytest.mark.parametrize( + ("exception", "config_entry_state"), + [ + (LetPotAuthenticationException, ConfigEntryState.SETUP_ERROR), + (LetPotConnectionException, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_get_devices_exceptions( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + exception: Exception, + config_entry_state: ConfigEntryState, +) -> None: + """Test config entry errors if an exception is raised when getting devices.""" + mock_client.get_devices.side_effect = exception + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is config_entry_state + mock_client.get_devices.assert_called_once() + mock_device_client.subscribe.assert_not_called() + + +async def test_device_subscribe_authentication_exception( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, +) -> None: + """Test config entry errors if it is not allowed to subscribe to device updates.""" + mock_device_client.subscribe.side_effect = LetPotAuthenticationException + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + mock_device_client.subscribe.assert_called_once() + mock_device_client.get_current_status.assert_not_called() + + +async def test_device_refresh_exception( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, +) -> None: + """Test config entry errors with retry if getting a device state update fails.""" + mock_device_client.get_current_status.side_effect = LetPotException + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + mock_device_client.get_current_status.assert_called_once() diff --git a/tests/components/letpot/test_sensor.py b/tests/components/letpot/test_sensor.py new file mode 100644 index 00000000000..a527d062ca7 --- /dev/null +++ b/tests/components/letpot/test_sensor.py @@ -0,0 +1,28 @@ +"""Test sensor entities for the LetPot integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/letpot/test_switch.py b/tests/components/letpot/test_switch.py new file mode 100644 index 00000000000..0ba1f556bc9 --- /dev/null +++ b/tests/components/letpot/test_switch.py @@ -0,0 +1,113 @@ +"""Test switch entities for the LetPot integration.""" + +from unittest.mock import MagicMock, patch + +from letpot.exceptions import LetPotConnectionException, LetPotException +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.switch import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test switch entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("service", "parameter_value"), + [ + ( + SERVICE_TURN_ON, + True, + ), + ( + SERVICE_TURN_OFF, + False, + ), + ( + SERVICE_TOGGLE, + False, # Mock switch is on after setup, toggle will turn off + ), + ], +) +async def test_set_switch( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + service: str, + parameter_value: bool, +) -> None: + """Test switch entity turned on/turned off/toggled.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + "switch", + service, + blocking=True, + target={"entity_id": "switch.garden_power"}, + ) + + mock_device_client.set_power.assert_awaited_once_with(parameter_value) + + +@pytest.mark.parametrize( + ("service", "exception", "user_error"), + [ + ( + SERVICE_TURN_ON, + LetPotConnectionException("Connection failed"), + "An error occurred while communicating with the LetPot device: Connection failed", + ), + ( + SERVICE_TURN_OFF, + LetPotException("Random thing failed"), + "An unknown error occurred while communicating with the LetPot device: Random thing failed", + ), + ], +) +async def test_switch_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + service: str, + exception: Exception, + user_error: str, +) -> None: + """Test switch entity exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_device_client.set_power.side_effect = exception + + assert hass.states.get("switch.garden_power") is not None + with pytest.raises(HomeAssistantError, match=user_error): + await hass.services.async_call( + "switch", + service, + blocking=True, + target={"entity_id": "switch.garden_power"}, + ) diff --git a/tests/components/letpot/test_time.py b/tests/components/letpot/test_time.py new file mode 100644 index 00000000000..e65ea4532e1 --- /dev/null +++ b/tests/components/letpot/test_time.py @@ -0,0 +1,90 @@ +"""Test time entities for the LetPot integration.""" + +from datetime import time +from unittest.mock import MagicMock, patch + +from letpot.exceptions import LetPotConnectionException, LetPotException +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.time import SERVICE_SET_VALUE +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_client: MagicMock, + mock_device_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test time entities.""" + with patch("homeassistant.components.letpot.PLATFORMS", [Platform.TIME]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_time( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, +) -> None: + """Test setting the time entity.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + "time", + SERVICE_SET_VALUE, + service_data={"time": time(hour=7, minute=0)}, + blocking=True, + target={"entity_id": "time.garden_light_on"}, + ) + + mock_device_client.set_light_schedule.assert_awaited_once_with(time(7, 0), None) + + +@pytest.mark.parametrize( + ("exception", "user_error"), + [ + ( + LetPotConnectionException("Connection failed"), + "An error occurred while communicating with the LetPot device: Connection failed", + ), + ( + LetPotException("Random thing failed"), + "An unknown error occurred while communicating with the LetPot device: Random thing failed", + ), + ], +) +async def test_time_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + exception: Exception, + user_error: str, +) -> None: + """Test time entity exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_device_client.set_light_schedule.side_effect = exception + + assert hass.states.get("time.garden_light_on") is not None + with pytest.raises(HomeAssistantError, match=user_error): + await hass.services.async_call( + "time", + SERVICE_SET_VALUE, + service_data={"time": time(hour=7, minute=0)}, + blocking=True, + target={"entity_id": "time.garden_light_on"}, + ) diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/profile.json b/tests/components/lg_thinq/fixtures/air_conditioner/profile.json index 0d45dc5c9f4..85ce95da0ed 100644 --- a/tests/components/lg_thinq/fixtures/air_conditioner/profile.json +++ b/tests/components/lg_thinq/fixtures/air_conditioner/profile.json @@ -57,6 +57,16 @@ "type": "number" } }, + "filterInfo": { + "filterLifetime": { + "mode": ["r"], + "type": "number" + }, + "usedTime": { + "mode": ["r"], + "type": "number" + } + }, "operation": { "airCleanOperationMode": { "mode": ["w"], @@ -124,6 +134,52 @@ } } }, + "temperatureInUnits": [ + { + "currentTemperature": { + "type": "number", + "mode": ["r"] + }, + "targetTemperature": { + "type": "number", + "mode": ["r"] + }, + "coolTargetTemperature": { + "type": "range", + "mode": ["w"], + "value": { + "w": { + "max": 30, + "min": 18, + "step": 1 + } + } + }, + "unit": "C" + }, + { + "currentTemperature": { + "type": "number", + "mode": ["r"] + }, + "targetTemperature": { + "type": "number", + "mode": ["r"] + }, + "coolTargetTemperature": { + "type": "range", + "mode": ["w"], + "value": { + "w": { + "max": 86, + "min": 64, + "step": 2 + } + } + }, + "unit": "F" + } + ], "timer": { "relativeHourToStart": { "mode": ["r", "w"], @@ -149,6 +205,24 @@ "mode": ["r", "w"], "type": "number" } + }, + "windDirection": { + "rotateUpDown": { + "type": "boolean", + "mode": ["r", "w"], + "value": { + "r": [true, false], + "w": [true, false] + } + }, + "rotateLeftRight": { + "type": "boolean", + "mode": ["r", "w"], + "value": { + "r": [true, false], + "w": [true, false] + } + } } } } diff --git a/tests/components/lg_thinq/fixtures/air_conditioner/status.json b/tests/components/lg_thinq/fixtures/air_conditioner/status.json index 90d15d1ae16..8440e7da28c 100644 --- a/tests/components/lg_thinq/fixtures/air_conditioner/status.json +++ b/tests/components/lg_thinq/fixtures/air_conditioner/status.json @@ -32,6 +32,19 @@ "targetTemperature": 19, "unit": "C" }, + "temperatureInUnits": [ + { + "currentTemperature": 25, + "targetTemperature": 19, + "unit": "C" + }, + { + "currentTemperature": 77, + "targetTemperature": 66, + "unit": "F" + } + ], + "timer": { "relativeStartTimer": "UNSET", "relativeStopTimer": "UNSET", @@ -39,5 +52,9 @@ "absoluteStopTimer": "UNSET", "absoluteHourToStart": 13, "absoluteMinuteToStart": 14 + }, + "windDirection": { + "rotateUpDown": false, + "rotateLeftRight": false } } diff --git a/tests/components/lg_thinq/snapshots/test_climate.ambr b/tests/components/lg_thinq/snapshots/test_climate.ambr index e9470c3de03..e2fcc2540f3 100644 --- a/tests/components/lg_thinq/snapshots/test_climate.ambr +++ b/tests/components/lg_thinq/snapshots/test_climate.ambr @@ -23,6 +23,7 @@ 'target_temp_step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -43,7 +44,7 @@ 'original_name': None, 'platform': 'lg_thinq', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_climate_air_conditioner', 'unit_of_measurement': None, @@ -72,7 +73,9 @@ 'preset_modes': list([ 'air_clean', ]), - 'supported_features': , + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, 'target_temp_step': 1, 'temperature': 19, }), diff --git a/tests/components/lg_thinq/snapshots/test_event.ambr b/tests/components/lg_thinq/snapshots/test_event.ambr index 025f4496aeb..dbb43ce0bb9 100644 --- a/tests/components/lg_thinq/snapshots/test_event.ambr +++ b/tests/components/lg_thinq/snapshots/test_event.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lg_thinq/snapshots/test_number.ambr b/tests/components/lg_thinq/snapshots/test_number.ambr index 68f01854501..ef4d9a21b86 100644 --- a/tests/components/lg_thinq/snapshots/test_number.ambr +++ b/tests/components/lg_thinq/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr index 387df916eba..5e6eb98ac42 100644 --- a/tests/components/lg_thinq/snapshots/test_sensor.ambr +++ b/tests/components/lg_thinq/snapshots/test_sensor.ambr @@ -1,4 +1,52 @@ # serializer version: 1 +# name: test_all_entities[sensor.test_air_conditioner_filter_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_filter_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Filter remaining', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_filter_lifetime', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_filter_remaining-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test air conditioner Filter remaining', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_filter_remaining', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '540', + }) +# --- # name: test_all_entities[sensor.test_air_conditioner_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -8,6 +56,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +108,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +160,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +212,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -203,3 +255,149 @@ 'state': '24', }) # --- +# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_schedule_turn_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule turn-off', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_to_stop', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Test air conditioner Schedule turn-off', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_schedule_turn_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_schedule_turn_on', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule turn-on', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_to_start', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Test air conditioner Schedule turn-on', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_schedule_turn_on', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_air_conditioner_schedule_turn_on_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Schedule turn-on', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_absolute_to_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.test_air_conditioner_schedule_turn_on_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test air conditioner Schedule turn-on', + }), + 'context': , + 'entity_id': 'sensor.test_air_conditioner_schedule_turn_on_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-10-10T13:14:00+00:00', + }) +# --- diff --git a/tests/components/lg_thinq/test_sensor.py b/tests/components/lg_thinq/test_sensor.py index 02b91b4771b..e1f1a7ed93d 100644 --- a/tests/components/lg_thinq/test_sensor.py +++ b/tests/components/lg_thinq/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the LG Thinq sensor platform.""" +from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest @@ -15,6 +16,7 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.freeze_time(datetime(2024, 10, 10, tzinfo=UTC)) async def test_all_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -23,6 +25,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" + hass.config.time_zone = "UTC" with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.SENSOR]): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index d1a6920f84a..e2a35bcb1b1 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -8,7 +8,6 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp, zeroconf from homeassistant.components.lifx import DOMAIN from homeassistant.components.lifx.config_flow import LifXConfigFlow from homeassistant.components.lifx.const import CONF_SERIAL @@ -16,6 +15,11 @@ from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from homeassistant.setup import async_setup_component from . import ( @@ -362,7 +366,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC, hostname=LABEL ), ) @@ -385,7 +389,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=IP_ADDRESS, macaddress="000000000000", hostname="mock_hostname" ), ) @@ -402,7 +406,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.5", macaddress="000000000001", hostname="mock_hostname" ), ) @@ -416,19 +420,19 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC, hostname=LABEL ), ), ( config_entries.SOURCE_HOMEKIT, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname=LABEL, name=LABEL, port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + properties={ATTR_PROPERTIES_ID: "any"}, type="mock_type", ), ), @@ -476,19 +480,19 @@ async def test_discovered_by_dhcp_or_discovery( [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC, hostname=LABEL ), ), ( config_entries.SOURCE_HOMEKIT, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname=LABEL, name=LABEL, port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + properties={ATTR_PROPERTIES_ID: "any"}, type="mock_type", ), ), @@ -520,19 +524,19 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC, hostname=LABEL ), ), ( config_entries.SOURCE_HOMEKIT, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname=LABEL, name=LABEL, port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + properties={ATTR_PROPERTIES_ID: "any"}, type="mock_type", ), ), diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index ffe819fa2cb..58843d63f9a 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -25,6 +25,7 @@ from homeassistant.components.lifx.manager import ( SERVICE_EFFECT_MORPH, SERVICE_EFFECT_MOVE, SERVICE_EFFECT_SKY, + SERVICE_PAINT_THEME, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -1045,6 +1046,104 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: bulb.set_power.reset_mock() +@pytest.mark.usefixtures("mock_discovery") +async def test_paint_theme_service(hass: HomeAssistant) -> None: + """Test the firmware flame and morph effects on a matrix device.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.power_level = 0 + bulb.color = [65535, 65535, 65535, 65535] + with ( + _patch_discovery(device=bulb), + _patch_config_flow_try_connect(device=bulb), + _patch_device(device=bulb), + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + SERVICE_PAINT_THEME, + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 4, ATTR_THEME: "autumn"}, + blocking=True, + ) + + bulb.power_level = 65535 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_color.calls) == 1 + call_dict = bulb.set_color.calls[0][1] + call_dict.pop("callb") + assert call_dict["value"] in [ + (5643, 65535, 32768, 3500), + (15109, 65535, 32768, 3500), + (8920, 65535, 32768, 3500), + (10558, 65535, 32768, 3500), + ] + assert call_dict["duration"] == 4000 + bulb.set_color.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + SERVICE_PAINT_THEME, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 6, + ATTR_PALETTE: [ + (0, 100, 255, 3500), + (60, 100, 255, 3500), + (120, 100, 255, 3500), + (180, 100, 255, 3500), + (240, 100, 255, 3500), + (300, 100, 255, 3500), + ], + }, + blocking=True, + ) + + bulb.power_level = 65535 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + assert len(bulb.set_power.calls) == 1 + assert len(bulb.set_color.calls) == 1 + call_dict = bulb.set_color.calls[0][1] + call_dict.pop("callb") + hue = round(call_dict["value"][0] / 65535 * 360) + sat = round(call_dict["value"][1] / 65535 * 100) + bri = call_dict["value"][2] >> 8 + kel = call_dict["value"][3] + assert (hue, sat, bri, kel) in [ + (0, 100, 255, 3500), + (60, 100, 255, 3500), + (120, 100, 255, 3500), + (180, 100, 255, 3500), + (240, 100, 255, 3500), + (300, 100, 255, 3500), + ] + assert call_dict["duration"] == 6000 + + bulb.set_color.reset_mock() + bulb.set_power.reset_mock() + + async def test_color_light_with_temp( hass: HomeAssistant, mock_effect_conductor ) -> None: diff --git a/tests/components/light/common.py b/tests/components/light/common.py index b29ac0c7c89..77411cd637d 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -26,7 +26,6 @@ from homeassistant.components.light import ( DOMAIN, ColorMode, LightEntity, - LightEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -157,7 +156,7 @@ class MockLight(MockToggleEntity, LightEntity): _attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN _attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN - supported_features = LightEntityFeature(0) + supported_features = 0 brightness = None color_temp_kelvin = None diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 94e12ffbfa5..2a5c9f0bb18 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import MockLight diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 4e8414edabc..ae54bbd2512 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 303bf68f68c..5bc17ea3e24 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,6 +1,7 @@ """The tests for the Light component.""" from types import ModuleType +from typing import Literal from unittest.mock import MagicMock, mock_open, patch import pytest @@ -22,7 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import frame from homeassistant.setup import async_setup_component -import homeassistant.util.color as color_util +from homeassistant.util import color as color_util from .common import MockLight @@ -137,8 +138,13 @@ async def test_services( ent3.supported_color_modes = [light.ColorMode.HS] ent1.supported_features = light.LightEntityFeature.TRANSITION ent2.supported_features = ( - light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION + light.SUPPORT_COLOR + | light.LightEntityFeature.EFFECT + | light.LightEntityFeature.TRANSITION ) + # Set color modes to none to trigger backwards compatibility in LightEntity + ent2.supported_color_modes = None + ent2.color_mode = None ent3.supported_features = ( light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION ) @@ -254,7 +260,10 @@ async def test_services( } _, data = ent2.last_call("turn_on") - assert data == {light.ATTR_EFFECT: "fun_effect"} + assert data == { + light.ATTR_EFFECT: "fun_effect", + light.ATTR_HS_COLOR: (0, 0), + } _, data = ent3.last_call("turn_on") assert data == {light.ATTR_FLASH: "short", light.ATTR_HS_COLOR: (71.059, 100)} @@ -338,6 +347,8 @@ async def test_services( _, data = ent2.last_call("turn_on") assert data == { + light.ATTR_BRIGHTNESS: 100, + light.ATTR_HS_COLOR: profile.hs_color, light.ATTR_TRANSITION: 1, } @@ -915,12 +926,16 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: setup_test_component_platform(hass, light.DOMAIN, entities) entity0 = entities[0] - entity0.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity0.color_mode = light.ColorMode.BRIGHTNESS + entity0.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity0.supported_color_modes = None + entity0.color_mode = None entity0.brightness = 100 entity1 = entities[1] - entity1.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity1.color_mode = light.ColorMode.BRIGHTNESS + entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None entity1.brightness = 50 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -981,8 +996,10 @@ async def test_light_brightness_pct_conversion( setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) entity = mock_light_entities[0] - entity.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity.color_mode = light.ColorMode.BRIGHTNESS + entity.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity.supported_color_modes = None + entity.color_mode = None entity.brightness = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1131,6 +1148,167 @@ invalid_no_brightness_no_color_no_transition,,, assert invalid_profile_name not in profiles.data +@pytest.mark.parametrize("light_state", [STATE_ON, STATE_OFF]) +async def test_light_backwards_compatibility_supported_color_modes( + hass: HomeAssistant, light_state: Literal["on", "off"] +) -> None: + """Test supported_color_modes if not implemented by the entity.""" + entities = [ + MockLight("Test_0", light_state), + MockLight("Test_1", light_state), + MockLight("Test_2", light_state), + MockLight("Test_3", light_state), + MockLight("Test_4", light_state), + ] + + entity0 = entities[0] + + entity1 = entities[1] + entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None + + entity2 = entities[2] + entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None + + entity3 = entities[3] + entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None + + entity4 = entities[4] + entity4.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP + ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None + + setup_test_component_platform(hass, light.DOMAIN, entities) + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.ONOFF] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.ONOFF + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.BRIGHTNESS] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.HS] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.ColorMode.COLOR_TEMP, + light.ColorMode.HS, + ] + if light_state == STATE_OFF: + assert state.attributes["color_mode"] is None + else: + assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN + + +async def test_light_backwards_compatibility_color_mode(hass: HomeAssistant) -> None: + """Test color_mode if not implemented by the entity.""" + entities = [ + MockLight("Test_0", STATE_ON), + MockLight("Test_1", STATE_ON), + MockLight("Test_2", STATE_ON), + MockLight("Test_3", STATE_ON), + MockLight("Test_4", STATE_ON), + ] + + entity0 = entities[0] + + entity1 = entities[1] + entity1.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity1.supported_color_modes = None + entity1.color_mode = None + entity1.brightness = 100 + + entity2 = entities[2] + entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + # Set color modes to none to trigger backwards compatibility in LightEntity + entity2.supported_color_modes = None + entity2.color_mode = None + entity2.color_temp_kelvin = 10000 + + entity3 = entities[3] + entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None + entity3.hs_color = (240, 100) + + entity4 = entities[4] + entity4.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP + ) + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None + entity4.hs_color = (240, 100) + entity4.color_temp_kelvin = 10000 + + setup_test_component_platform(hass, light.DOMAIN, entities) + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.ONOFF] + assert state.attributes["color_mode"] == light.ColorMode.ONOFF + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.BRIGHTNESS] + assert state.attributes["color_mode"] == light.ColorMode.BRIGHTNESS + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] + assert state.attributes["color_mode"] == light.ColorMode.COLOR_TEMP + assert state.attributes["rgb_color"] == (202, 218, 255) + assert state.attributes["hs_color"] == (221.575, 20.9) + assert state.attributes["xy_color"] == (0.278, 0.287) + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.HS] + assert state.attributes["color_mode"] == light.ColorMode.HS + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.ColorMode.COLOR_TEMP, + light.ColorMode.HS, + ] + # hs color prioritized over color_temp, light should report mode ColorMode.HS + assert state.attributes["color_mode"] == light.ColorMode.HS + + async def test_light_service_call_rgbw(hass: HomeAssistant) -> None: """Test rgbw functionality in service calls.""" entity0 = MockLight("Test_rgbw", STATE_ON) @@ -1186,7 +1364,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_onoff", "supported_color_modes": [light.ColorMode.ONOFF], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, } state = hass.states.get(entity1.entity_id) @@ -1194,7 +1372,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_brightness", "supported_color_modes": [light.ColorMode.BRIGHTNESS], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "brightness": None, } @@ -1203,7 +1381,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_ct", "supported_color_modes": [light.ColorMode.COLOR_TEMP], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "brightness": None, "color_temp": None, "color_temp_kelvin": None, @@ -1221,7 +1399,7 @@ async def test_light_state_off(hass: HomeAssistant) -> None: "color_mode": None, "friendly_name": "Test_rgbw", "supported_color_modes": [light.ColorMode.RGBW], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "brightness": None, "rgbw_color": None, "hs_color": None, @@ -1252,7 +1430,7 @@ async def test_light_state_rgbw(hass: HomeAssistant) -> None: "color_mode": light.ColorMode.RGBW, "friendly_name": "Test_rgbw", "supported_color_modes": [light.ColorMode.RGBW], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "hs_color": (240.0, 25.0), "rgb_color": (3, 3, 4), "rgbw_color": (1, 2, 3, 4), @@ -1283,7 +1461,7 @@ async def test_light_state_rgbww(hass: HomeAssistant) -> None: "color_mode": light.ColorMode.RGBWW, "friendly_name": "Test_rgbww", "supported_color_modes": [light.ColorMode.RGBWW], - "supported_features": light.LightEntityFeature(0), + "supported_features": 0, "hs_color": (60.0, 20.0), "rgb_color": (5, 5, 4), "rgbww_color": (1, 2, 3, 4, 5), @@ -1299,6 +1477,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: MockLight("Test_rgb", STATE_ON), MockLight("Test_xy", STATE_ON), MockLight("Test_all", STATE_ON), + MockLight("Test_legacy", STATE_ON), MockLight("Test_rgbw", STATE_ON), MockLight("Test_rgbww", STATE_ON), MockLight("Test_temperature", STATE_ON), @@ -1322,13 +1501,19 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: } entity4 = entities[4] - entity4.supported_color_modes = {light.ColorMode.RGBW} + entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = entities[5] - entity5.supported_color_modes = {light.ColorMode.RGBWW} + entity5.supported_color_modes = {light.ColorMode.RGBW} entity6 = entities[6] - entity6.supported_color_modes = {light.ColorMode.COLOR_TEMP} + entity6.supported_color_modes = {light.ColorMode.RGBWW} + + entity7 = entities[7] + entity7.supported_color_modes = {light.ColorMode.COLOR_TEMP} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1350,12 +1535,15 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: ] state = hass.states.get(entity4.entity_id) - assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW] + assert state.attributes["supported_color_modes"] == [light.ColorMode.HS] state = hass.states.get(entity5.entity_id) - assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW] + assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW] state = hass.states.get(entity6.entity_id) + assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBWW] + + state = hass.states.get(entity7.entity_id) assert state.attributes["supported_color_modes"] == [light.ColorMode.COLOR_TEMP] await hass.services.async_call( @@ -1370,6 +1558,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 100, "hs_color": (240, 100), @@ -1385,10 +1574,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)} + assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)} + assert data == {"brightness": 255, "rgbw_color": (0, 0, 255, 0)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 255, "rgbww_color": (0, 0, 255, 0, 0)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 255, "color_temp_kelvin": 1739, "color_temp": 575} await hass.services.async_call( @@ -1403,6 +1594,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 100, "hs_color": (240, 0), @@ -1418,11 +1610,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} + assert data == {"brightness": 255, "hs_color": (240.0, 0.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} + _, data = entity6.last_call("turn_on") # The midpoint of the white channels is warm, compensated by adding green + blue assert data == {"brightness": 255, "rgbww_color": (0, 76, 141, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 255, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1437,6 +1631,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgb_color": (128, 0, 0), @@ -1451,12 +1646,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: assert data == {"brightness": 128, "xy_color": (0.701, 0.299)} _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (128, 0, 0)} - _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)} + assert data == {"brightness": 128, "hs_color": (0.0, 100.0)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)} + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 0)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 0, 0)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 6279, "color_temp": 159} await hass.services.async_call( @@ -1471,6 +1667,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgb_color": (255, 255, 255), @@ -1486,11 +1683,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (0, 0, 0, 255)} + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (0, 0, 0, 255)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1505,6 +1704,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "xy_color": (0.1, 0.8), @@ -1520,10 +1720,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "xy_color": (0.1, 0.8)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)} + assert data == {"brightness": 128, "hs_color": (125.176, 100.0)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)} + assert data == {"brightness": 128, "rgbw_color": (0, 255, 22, 0)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (0, 255, 22, 0, 0)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 8645, "color_temp": 115} await hass.services.async_call( @@ -1538,6 +1740,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "xy_color": (0.323, 0.329), @@ -1553,11 +1756,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "xy_color": (0.323, 0.329)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (1, 0, 0, 255)} + assert data == {"brightness": 128, "hs_color": (0.0, 0.392)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (1, 0, 0, 255)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1572,6 +1777,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbw_color": (128, 0, 0, 64), @@ -1587,11 +1793,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (128, 43, 43)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} + assert data == {"brightness": 128, "hs_color": (0.0, 66.406)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3011, "color_temp": 332} await hass.services.async_call( @@ -1606,6 +1814,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbw_color": (255, 255, 255, 255), @@ -1621,11 +1830,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (255, 255, 255)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} + assert data == {"brightness": 128, "hs_color": (0.0, 0.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} + _, data = entity6.last_call("turn_on") # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} - _, data = entity6.last_call("turn_on") + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} await hass.services.async_call( @@ -1640,6 +1851,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbww_color": (128, 0, 0, 64, 32), @@ -1655,10 +1867,12 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (128, 33, 26)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)} + assert data == {"brightness": 128, "hs_color": (4.118, 79.688)} _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)} + assert data == {"brightness": 128, "rgbw_color": (128, 9, 0, 33)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (128, 0, 0, 64, 32)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3845, "color_temp": 260} await hass.services.async_call( @@ -1673,6 +1887,7 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: entity4.entity_id, entity5.entity_id, entity6.entity_id, + entity7.entity_id, ], "brightness_pct": 50, "rgbww_color": (255, 255, 255, 255, 255), @@ -1688,11 +1903,13 @@ async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: _, data = entity3.last_call("turn_on") assert data == {"brightness": 128, "rgb_color": (255, 217, 185)} _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} + _, data = entity5.last_call("turn_on") # The midpoint the white channels is warm, compensated by decreasing green + blue assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)} - _, data = entity5.last_call("turn_on") - assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} _, data = entity6.last_call("turn_on") + assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} + _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3451, "color_temp": 289} @@ -1705,6 +1922,7 @@ async def test_light_service_call_color_conversion_named_tuple( MockLight("Test_rgb", STATE_ON), MockLight("Test_xy", STATE_ON), MockLight("Test_all", STATE_ON), + MockLight("Test_legacy", STATE_ON), MockLight("Test_rgbw", STATE_ON), MockLight("Test_rgbww", STATE_ON), ] @@ -1727,10 +1945,16 @@ async def test_light_service_call_color_conversion_named_tuple( } entity4 = entities[4] - entity4.supported_color_modes = {light.ColorMode.RGBW} + entity4.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity4.supported_color_modes = None + entity4.color_mode = None entity5 = entities[5] - entity5.supported_color_modes = {light.ColorMode.RGBWW} + entity5.supported_color_modes = {light.ColorMode.RGBW} + + entity6 = entities[6] + entity6.supported_color_modes = {light.ColorMode.RGBWW} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1746,6 +1970,7 @@ async def test_light_service_call_color_conversion_named_tuple( entity3.entity_id, entity4.entity_id, entity5.entity_id, + entity6.entity_id, ], "brightness_pct": 25, "rgb_color": color_util.RGBColor(128, 0, 0), @@ -1761,8 +1986,10 @@ async def test_light_service_call_color_conversion_named_tuple( _, data = entity3.last_call("turn_on") assert data == {"brightness": 64, "rgb_color": (128, 0, 0)} _, data = entity4.last_call("turn_on") - assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + assert data == {"brightness": 64, "hs_color": (0.0, 100.0)} _, data = entity5.last_call("turn_on") + assert data == {"brightness": 64, "rgbw_color": (128, 0, 0, 0)} + _, data = entity6.last_call("turn_on") assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)} @@ -2131,6 +2358,13 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None: entity2.rgb_color = "Invalid" # Should be ignored entity2.xy_color = (0.1, 0.8) + entity3 = entities[3] + entity3.hs_color = (240, 100) + entity3.supported_features = light.SUPPORT_COLOR + # Set color modes to none to trigger backwards compatibility in LightEntity + entity3.supported_color_modes = None + entity3.color_mode = None + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -2152,6 +2386,12 @@ async def test_light_state_color_conversion(hass: HomeAssistant) -> None: assert state.attributes["rgb_color"] == (0, 255, 22) assert state.attributes["xy_color"] == (0.1, 0.8) + state = hass.states.get(entity3.entity_id) + assert state.attributes["color_mode"] == light.ColorMode.HS + assert state.attributes["hs_color"] == (240, 100) + assert state.attributes["rgb_color"] == (0, 0, 255) + assert state.attributes["xy_color"] == (0.136, 0.04) + async def test_services_filter_parameters( hass: HomeAssistant, @@ -2386,6 +2626,31 @@ def test_filter_supported_color_modes() -> None: assert light.filter_supported_color_modes(supported) == {light.ColorMode.BRIGHTNESS} +def test_deprecated_supported_features_ints( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test deprecated supported features ints.""" + + class MockLightEntityEntity(light.LightEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockLightEntityEntity() + entity.hass = hass + entity.platform = MockEntityPlatform(hass, domain="test", platform_name="test") + assert entity.supported_features_compat is light.LightEntityFeature(1) + assert "MockLightEntityEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "LightEntityFeature" in caplog.text + assert "and color modes" in caplog.text + caplog.clear() + assert entity.supported_features_compat is light.LightEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text + + @pytest.mark.parametrize( ("color_mode", "supported_color_modes", "warning_expected"), [ @@ -2393,7 +2658,7 @@ def test_filter_supported_color_modes() -> None: (light.ColorMode.ONOFF, {light.ColorMode.ONOFF}, False), ], ) -def test_report_no_color_mode( +async def test_report_no_color_mode( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, color_mode: str, @@ -2409,6 +2674,8 @@ def test_report_no_color_mode( _attr_supported_color_modes = supported_color_modes entity = MockLightEntityEntity() + platform = MockEntityPlatform(hass, domain="test", platform_name="test") + await platform.async_add_entities([entity]) entity._async_calculate_state() expected_warning = "does not report a color mode" assert (expected_warning in caplog.text) is warning_expected @@ -2421,7 +2688,7 @@ def test_report_no_color_mode( (light.ColorMode.ONOFF, {light.ColorMode.ONOFF}, False), ], ) -def test_report_no_color_modes( +async def test_report_no_color_modes( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, color_mode: str, @@ -2437,6 +2704,8 @@ def test_report_no_color_modes( _attr_supported_color_modes = supported_color_modes entity = MockLightEntityEntity() + platform = MockEntityPlatform(hass, domain="test", platform_name="test") + await platform.async_add_entities([entity]) entity._async_calculate_state() expected_warning = "does not set supported color modes" assert (expected_warning in caplog.text) is warning_expected @@ -2467,7 +2736,7 @@ def test_report_no_color_modes( (light.ColorMode.HS, {light.ColorMode.BRIGHTNESS}, "effect", True), ], ) -def test_report_invalid_color_mode( +async def test_report_invalid_color_mode( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, color_mode: str, @@ -2485,6 +2754,8 @@ def test_report_invalid_color_mode( _attr_supported_color_modes = supported_color_modes entity = MockLightEntityEntity() + platform = MockEntityPlatform(hass, domain="test", platform_name="test") + await platform.async_add_entities([entity]) entity._async_calculate_state() expected_warning = f"set to unsupported color mode {color_mode}" assert (expected_warning in caplog.text) is warning_expected diff --git a/tests/components/linear_garage_door/snapshots/test_cover.ambr b/tests/components/linear_garage_door/snapshots/test_cover.ambr index 96745e1d92a..a09156c53e0 100644 --- a/tests/components/linear_garage_door/snapshots/test_cover.ambr +++ b/tests/components/linear_garage_door/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/linear_garage_door/snapshots/test_light.ambr b/tests/components/linear_garage_door/snapshots/test_light.ambr index ba64a2b0a04..9e27efc02ec 100644 --- a/tests/components/linear_garage_door/snapshots/test_light.ambr +++ b/tests/components/linear_garage_door/snapshots/test_light.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -122,6 +124,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -178,6 +181,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py index 640264eb207..8f1e85f28ff 100644 --- a/tests/components/linear_garage_door/test_init.py +++ b/tests/components/linear_garage_door/test_init.py @@ -5,8 +5,10 @@ from unittest.mock import AsyncMock from linear_garage_door import InvalidLoginError import pytest +from homeassistant.components.linear_garage_door.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from . import setup_integration @@ -51,3 +53,23 @@ async def test_setup_failure( await setup_integration(hass, mock_config_entry, []) assert mock_config_entry.state == entry_state + + +async def test_repair_issue( + hass: HomeAssistant, + mock_linear: AsyncMock, + mock_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test reauth trigger setup.""" + + await setup_integration(hass, mock_config_entry, []) + assert mock_config_entry.state is ConfigEntryState.LOADED + + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None diff --git a/tests/components/linkplay/test_config_flow.py b/tests/components/linkplay/test_config_flow.py index 3fd1fbea95e..adf6aa601ae 100644 --- a/tests/components/linkplay/test_config_flow.py +++ b/tests/components/linkplay/test_config_flow.py @@ -7,11 +7,11 @@ from linkplay.exceptions import LinkPlayRequestException import pytest from homeassistant.components.linkplay.const import DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .conftest import HOST, HOST_REENTRY, NAME, UUID diff --git a/tests/components/litejet/conftest.py b/tests/components/litejet/conftest.py index 41517acf1e9..975f943d2fa 100644 --- a/tests/components/litejet/conftest.py +++ b/tests/components/litejet/conftest.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util @pytest.fixture diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index c13fda9068c..de99d701926 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -11,7 +11,7 @@ import pytest from homeassistant import setup from homeassistant.components import automation from homeassistant.core import HomeAssistant, ServiceCall -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import async_init_integration diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index 8849392b3dd..d96ce06ca59 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -90,6 +90,14 @@ ROBOT_4_DATA = { "isUSBPowerOn": True, "USBFaultStatus": "CLEAR", "isDFIPartialFull": True, + "isLaserDirty": False, + "surfaceType": "TILE", + "hopperStatus": None, + "scoopsSavedCount": 3769, + "isHopperRemoved": None, + "optimalLitterLevel": 450, + "litterLevelPercentage": 0.7, + "litterLevelState": "OPTIMAL", } FEEDER_ROBOT_DATA = { "id": 1, @@ -142,5 +150,15 @@ FEEDER_ROBOT_DATA = { }, ], } +PET_DATA = { + "petId": "PET-123", + "userId": "1234567", + "createdAt": "2023-04-27T23:26:49.813Z", + "name": "Kitty", + "type": "CAT", + "gender": "FEMALE", + "lastWeightReading": 9.1, + "breeds": ["sphynx"], +} VACUUM_ENTITY_ID = "vacuum.test_litter_box" diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 181e4fc1a90..d22c4b2ec49 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -5,14 +5,20 @@ from __future__ import annotations from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from pylitterbot import Account, FeederRobot, LitterRobot3, LitterRobot4, Robot +from pylitterbot import Account, FeederRobot, LitterRobot3, LitterRobot4, Pet, Robot from pylitterbot.exceptions import InvalidCommandException import pytest -from homeassistant.components import litterrobot from homeassistant.core import HomeAssistant -from .common import CONFIG, FEEDER_ROBOT_DATA, ROBOT_4_DATA, ROBOT_DATA +from .common import ( + CONFIG, + DOMAIN, + FEEDER_ROBOT_DATA, + PET_DATA, + ROBOT_4_DATA, + ROBOT_DATA, +) from tests.common import MockConfigEntry @@ -51,6 +57,7 @@ def create_mock_account( skip_robots: bool = False, v4: bool = False, feeder: bool = False, + pet: bool = False, ) -> MagicMock: """Create a mock Litter-Robot account.""" account = MagicMock(spec=Account) @@ -61,6 +68,7 @@ def create_mock_account( if skip_robots else [create_mock_robot(robot_data, account, v4, feeder, side_effect)] ) + account.pets = [Pet(PET_DATA, account.session)] if pet else [] return account @@ -82,6 +90,12 @@ def mock_account_with_feederrobot() -> MagicMock: return create_mock_account(feeder=True) +@pytest.fixture +def mock_account_with_pet() -> MagicMock: + """Mock account with Feeder-Robot.""" + return create_mock_account(pet=True) + + @pytest.fixture def mock_account_with_no_robots() -> MagicMock: """Mock a Litter-Robot account.""" @@ -117,22 +131,16 @@ def mock_account_with_side_effects() -> MagicMock: async def setup_integration( hass: HomeAssistant, mock_account: MagicMock, platform_domain: str | None = None ) -> MockConfigEntry: - """Load a Litter-Robot platform with the provided hub.""" + """Load a Litter-Robot platform with the provided coordinator.""" entry = MockConfigEntry( - domain=litterrobot.DOMAIN, - data=CONFIG[litterrobot.DOMAIN], + domain=DOMAIN, + data=CONFIG[DOMAIN], ) entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.litterrobot.hub.Account", - return_value=mock_account, - ), - patch( - "homeassistant.components.litterrobot.PLATFORMS_BY_TYPE", - {Robot: (platform_domain,)} if platform_domain else {}, - ), + with patch( + "homeassistant.components.litterrobot.coordinator.Account", + return_value=mock_account, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_binary_sensor.py b/tests/components/litterrobot/test_binary_sensor.py index 69b3f7ce3ab..3fe72aef7e3 100644 --- a/tests/components/litterrobot/test_binary_sensor.py +++ b/tests/components/litterrobot/test_binary_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.binary_sensor import ( - DOMAIN as PLATFORM_DOMAIN, + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, ) from homeassistant.const import ATTR_DEVICE_CLASS @@ -21,7 +21,7 @@ async def test_binary_sensors( mock_account: MagicMock, ) -> None: """Tests binary sensors.""" - await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account, BINARY_SENSOR_DOMAIN) state = hass.states.get("binary_sensor.test_sleeping") assert state.state == "off" diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index 9420d3cb8a8..caaf832b780 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -4,9 +4,9 @@ from unittest.mock import patch from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException +import pytest from homeassistant import config_entries -from homeassistant.components import litterrobot from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -16,9 +16,8 @@ from .common import CONF_USERNAME, CONFIG, DOMAIN from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, mock_account) -> None: - """Test we get the form.""" - +async def test_full_flow(hass: HomeAssistant, mock_account) -> None: + """Test full flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -35,99 +34,59 @@ async def test_form(hass: HomeAssistant, mock_account) -> None: return_value=True, ) as mock_setup_entry, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG[DOMAIN] ) - await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == CONFIG[DOMAIN][CONF_USERNAME] - assert result2["data"] == CONFIG[DOMAIN] + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == CONFIG[DOMAIN][CONF_USERNAME] + assert result["data"] == CONFIG[DOMAIN] assert len(mock_setup_entry.mock_calls) == 1 async def test_already_configured(hass: HomeAssistant) -> None: - """Test we handle already configured.""" + """Test already configured case.""" MockConfigEntry( - domain=litterrobot.DOMAIN, - data=CONFIG[litterrobot.DOMAIN], + domain=DOMAIN, + data=CONFIG[DOMAIN], ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=CONFIG[litterrobot.DOMAIN], + data=CONFIG[DOMAIN], ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_form_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" +@pytest.mark.parametrize( + ("side_effect", "connect_errors"), + [ + (Exception, {"base": "unknown"}), + (LitterRobotLoginException, {"base": "invalid_auth"}), + (LitterRobotException, {"base": "cannot_connect"}), + ], +) +async def test_create_entry( + hass: HomeAssistant, mock_account, side_effect, connect_errors +) -> None: + """Test creating an entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.litterrobot.config_flow.Account.connect", - side_effect=LitterRobotLoginException, + side_effect=side_effect, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG[DOMAIN] ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.litterrobot.config_flow.Account.connect", - side_effect=LitterRobotException, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG[DOMAIN] - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_unknown_error(hass: HomeAssistant) -> None: - """Test we handle unknown error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - with patch( - "homeassistant.components.litterrobot.config_flow.Account.connect", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG[DOMAIN] - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} - - -async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: - """Test the reauth flow.""" - entry = MockConfigEntry( - domain=litterrobot.DOMAIN, - data=CONFIG[litterrobot.DOMAIN], - ) - entry.add_to_hass(hass) - - result = await entry.start_reauth_flow(hass) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["errors"] == connect_errors with ( patch( @@ -137,22 +96,22 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: patch( "homeassistant.components.litterrobot.async_setup_entry", return_value=True, - ) as mock_setup_entry, + ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, + result["flow_id"], CONFIG[DOMAIN] ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == CONFIG[DOMAIN][CONF_USERNAME] + assert result["data"] == CONFIG[DOMAIN] -async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> None: - """Test the reauth flow fails and recovers.""" +async def test_reauth(hass: HomeAssistant, mock_account: Account) -> None: + """Test reauth flow (with fail and recover).""" entry = MockConfigEntry( - domain=litterrobot.DOMAIN, - data=CONFIG[litterrobot.DOMAIN], + domain=DOMAIN, + data=CONFIG[DOMAIN], ) entry.add_to_hass(hass) @@ -167,7 +126,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, + user_input={CONF_PASSWORD: CONFIG[DOMAIN][CONF_PASSWORD]}, ) assert result["type"] is FlowResultType.FORM @@ -185,7 +144,7 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, + user_input={CONF_PASSWORD: CONFIG[DOMAIN][CONF_PASSWORD]}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 1c8e0742b26..e42bdb048b7 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import pytest -from homeassistant.components import litterrobot from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_START, @@ -17,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from .common import CONFIG, VACUUM_ENTITY_ID +from .common import CONFIG, DOMAIN, VACUUM_ENTITY_ID from .conftest import setup_integration from tests.common import MockConfigEntry @@ -57,13 +56,13 @@ async def test_entry_not_setup( ) -> None: """Test being able to handle config entry not setup.""" entry = MockConfigEntry( - domain=litterrobot.DOMAIN, - data=CONFIG[litterrobot.DOMAIN], + domain=DOMAIN, + data=CONFIG[DOMAIN], ) entry.add_to_hass(hass) with patch( - "homeassistant.components.litterrobot.hub.Account.connect", + "homeassistant.components.litterrobot.coordinator.Account.connect", side_effect=side_effect, ): await hass.config_entries.async_setup(entry.entry_id) @@ -91,7 +90,7 @@ async def test_device_remove_devices( dead_device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={(litterrobot.DOMAIN, "test-serial", "remove-serial")}, + identifiers={(DOMAIN, "test-serial", "remove-serial")}, ) response = await client.remove_device(dead_device_entry.id, config_entry.entry_id) assert response["success"] diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index 48ec1bb06a5..b4902a56e63 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.select import ( ATTR_OPTION, ATTR_OPTIONS, - DOMAIN as PLATFORM_DOMAIN, + DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) from homeassistant.const import ATTR_ENTITY_ID, EntityCategory @@ -26,7 +26,7 @@ async def test_wait_time_select( hass: HomeAssistant, mock_account, entity_registry: er.EntityRegistry ) -> None: """Tests the wait time select entity.""" - await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account, SELECT_DOMAIN) select = hass.states.get(SELECT_ENTITY_ID) assert select @@ -41,7 +41,7 @@ async def test_wait_time_select( data[ATTR_OPTION] = wait_time await hass.services.async_call( - PLATFORM_DOMAIN, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, data, blocking=True, @@ -52,7 +52,7 @@ async def test_wait_time_select( async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> None: """Tests the wait time select entity with invalid value.""" - await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account, SELECT_DOMAIN) select = hass.states.get(SELECT_ENTITY_ID) assert select @@ -61,7 +61,7 @@ async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> No with pytest.raises(ServiceValidationError): await hass.services.async_call( - PLATFORM_DOMAIN, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, data, blocking=True, @@ -75,7 +75,7 @@ async def test_panel_brightness_select( entity_registry: er.EntityRegistry, ) -> None: """Tests the wait time select entity.""" - await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account_with_litterrobot_4, SELECT_DOMAIN) select = hass.states.get(PANEL_BRIGHTNESS_ENTITY_ID) assert select @@ -94,7 +94,7 @@ async def test_panel_brightness_select( data[ATTR_OPTION] = option await hass.services.async_call( - PLATFORM_DOMAIN, + SELECT_DOMAIN, SERVICE_SELECT_OPTION, data, blocking=True, diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 360d13096a7..e290d96fcf4 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -104,3 +104,13 @@ async def test_feeder_robot_sensor( sensor = hass.states.get("sensor.test_food_level") assert sensor.state == "10" assert sensor.attributes["unit_of_measurement"] == PERCENTAGE + + +async def test_pet_weight_sensor( + hass: HomeAssistant, mock_account_with_pet: MagicMock +) -> None: + """Tests pet weight sensors.""" + await setup_integration(hass, mock_account_with_pet, PLATFORM_DOMAIN) + sensor = hass.states.get("sensor.kitty_weight") + assert sensor.state == "9.1" + assert sensor.attributes["unit_of_measurement"] == UnitOfMass.POUNDS diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index f18098ccf1d..911dfb3b880 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -8,11 +8,9 @@ from unittest.mock import MagicMock from pylitterbot import Robot import pytest -from homeassistant.components.litterrobot import DOMAIN from homeassistant.components.litterrobot.vacuum import SERVICE_SET_SLEEP_MODE from homeassistant.components.vacuum import ( - ATTR_STATUS, - DOMAIN as PLATFORM_DOMAIN, + DOMAIN as VACUUM_DOMAIN, SERVICE_START, SERVICE_STOP, VacuumActivity, @@ -21,7 +19,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir -from .common import VACUUM_ENTITY_ID +from .common import DOMAIN, VACUUM_ENTITY_ID from .conftest import setup_integration VACUUM_UNIQUE_ID = "LR3C012345-litter_box" @@ -35,49 +33,33 @@ async def test_vacuum( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_account: MagicMock ) -> None: """Tests the vacuum entity was set up.""" - entity_registry.async_get_or_create( - PLATFORM_DOMAIN, + VACUUM_DOMAIN, DOMAIN, VACUUM_UNIQUE_ID, - suggested_object_id=VACUUM_ENTITY_ID.replace(PLATFORM_DOMAIN, ""), + suggested_object_id=VACUUM_ENTITY_ID.replace(VACUUM_DOMAIN, ""), ) ent_reg_entry = entity_registry.async_get(VACUUM_ENTITY_ID) assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID - await setup_integration(hass, mock_account, PLATFORM_DOMAIN) - assert len(entity_registry.entities) == 1 + await setup_integration(hass, mock_account, VACUUM_DOMAIN) assert hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum assert vacuum.state == VacuumActivity.DOCKED - assert vacuum.attributes["is_sleeping"] is False ent_reg_entry = entity_registry.async_get(VACUUM_ENTITY_ID) assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID -async def test_vacuum_status_when_sleeping( - hass: HomeAssistant, mock_account_with_sleeping_robot: MagicMock -) -> None: - """Tests the vacuum status when sleeping.""" - await setup_integration(hass, mock_account_with_sleeping_robot, PLATFORM_DOMAIN) - - vacuum = hass.states.get(VACUUM_ENTITY_ID) - assert vacuum - assert vacuum.attributes.get(ATTR_STATUS) == "Ready (Sleeping)" - - async def test_no_robots( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_account_with_no_robots: MagicMock, ) -> None: """Tests the vacuum entity was set up.""" - entry = await setup_integration(hass, mock_account_with_no_robots, PLATFORM_DOMAIN) - - assert not hass.services.has_service(DOMAIN, SERVICE_SET_SLEEP_MODE) + entry = await setup_integration(hass, mock_account_with_no_robots, VACUUM_DOMAIN) assert len(entity_registry.entities) == 0 @@ -89,7 +71,7 @@ async def test_vacuum_with_error( hass: HomeAssistant, mock_account_with_error: MagicMock ) -> None: """Tests a vacuum entity with an error.""" - await setup_integration(hass, mock_account_with_error, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account_with_error, VACUUM_DOMAIN) vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum @@ -114,7 +96,7 @@ async def test_activities( expected_state: str, ) -> None: """Test sending commands to the switch.""" - await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account_with_litterrobot_4, VACUUM_DOMAIN) robot: Robot = mock_account_with_litterrobot_4.robots[0] robot._update_data(robot_data, partial=True) @@ -147,7 +129,7 @@ async def test_commands( issue_registry: ir.IssueRegistry, ) -> None: """Test sending commands to the vacuum.""" - await setup_integration(hass, mock_account, PLATFORM_DOMAIN) + await setup_integration(hass, mock_account, VACUUM_DOMAIN) vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum @@ -158,7 +140,7 @@ async def test_commands( issues = extra.get("issues", set()) await hass.services.async_call( - COMPONENT_SERVICE_DOMAIN.get(service, PLATFORM_DOMAIN), + COMPONENT_SERVICE_DOMAIN.get(service, VACUUM_DOMAIN), service, data, blocking=True, diff --git a/tests/components/local_calendar/snapshots/test_diagnostics.ambr b/tests/components/local_calendar/snapshots/test_diagnostics.ambr index a70b9d7438b..14dc94ef919 100644 --- a/tests/components/local_calendar/snapshots/test_diagnostics.ambr +++ b/tests/components/local_calendar/snapshots/test_diagnostics.ambr @@ -6,12 +6,12 @@ PRODID:-//homeassistant.io//local_calendar 1.0//EN VERSION:*** BEGIN:VEVENT - DTSTAMP:20230313T190500 + DTSTAMP:20230313T190500Z UID:*** DTSTART:19970714T110000 DTEND:19970714T220000 SUMMARY:*** - CREATED:20230313T190500 + CREATED:20230313T190500Z RRULE:FREQ=DAILY SEQUENCE:*** END:VEVENT diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 61908faeca6..0720e6d7ded 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -8,7 +8,7 @@ import pytest from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.template import DATE_STR_FORMAT -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( FRIENDLY_NAME, diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index ddfdf4249bd..0eb48aa3060 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -281,7 +281,7 @@ async def test_import_from_yaml_fails( assert not hass.states.get("camera.config_test") issue = issue_registry.async_get_issue( - DOMAIN, f"no_access_path_{slugify("mock.file")}" + DOMAIN, f"no_access_path_{slugify('mock.file')}" ) assert issue assert issue.translation_key == "no_access_path" diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py index fd569b162bc..254a59cae0d 100644 --- a/tests/components/lock/conftest.py +++ b/tests/components/lock/conftest.py @@ -14,7 +14,7 @@ from homeassistant.components.lock import ( from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import ( MockConfigEntry, @@ -120,7 +120,7 @@ async def setup_lock_platform_test_entity( async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test lock platform via config entry.""" async_add_entities([entity]) diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 3ecdf2a9bca..7d1c39d10f0 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 68af8c7d482..510034a2172 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -21,7 +21,7 @@ from homeassistant.components.lock import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import UNDEFINED, UndefinedType from .conftest import MockLock diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index abb118467f4..b303a34e151 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -16,7 +16,7 @@ from homeassistant.components.recorder.models import ( from homeassistant.core import Context from homeassistant.helpers import entity_registry as er from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util IDX_TO_NAME = dict(enumerate(EventAsRow._fields)) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 841c8ed1247..c62bdcaa824 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -10,6 +10,7 @@ from freezegun import freeze_time import pytest import voluptuous as vol +from homeassistant import core as ha from homeassistant.components import logbook, recorder # pylint: disable-next=hass-component-root-import @@ -40,12 +41,11 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -import homeassistant.core as ha from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import MockRow, mock_humanify diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 50139d0f4f7..7b2550ccc82 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -37,7 +37,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.recorder.common import ( diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index cea0f969893..64dc1220683 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch from aiolookin import Climate, Device, Remote -from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo DEVICE_ID = "98F33163" MODULE = "homeassistant.components.lookin" diff --git a/tests/components/loqed/test_config_flow.py b/tests/components/loqed/test_config_flow.py index d59ed60796b..6f7da09fa0d 100644 --- a/tests/components/loqed/test_config_flow.py +++ b/tests/components/loqed/test_config_flow.py @@ -8,16 +8,16 @@ import aiohttp from loqedAPI import loqed from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.loqed.const import DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -zeroconf_data = zeroconf.ZeroconfServiceInfo( +zeroconf_data = ZeroconfServiceInfo( ip_address=ip_address("192.168.12.34"), ip_addresses=[ip_address("192.168.12.34")], hostname="LOQED-ffeeddccbbaa.local", diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 14d93d8302f..f35f7369f93 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -7,21 +7,12 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant.core import HomeAssistant +from homeassistant.helpers import frame from homeassistant.setup import async_setup_component from tests.typing import WebSocketGenerator -@pytest.fixture -def mock_onboarding_not_done() -> Generator[MagicMock]: - """Mock that Home Assistant is currently onboarding.""" - with patch( - "homeassistant.components.onboarding.async_is_onboarded", - return_value=False, - ) as mock_onboarding: - yield mock_onboarding - - @pytest.fixture def mock_onboarding_done() -> Generator[MagicMock]: """Mock that Home Assistant is currently onboarding.""" @@ -32,15 +23,6 @@ def mock_onboarding_done() -> Generator[MagicMock]: yield mock_onboarding -@pytest.fixture -def mock_add_onboarding_listener() -> Generator[MagicMock]: - """Mock that Home Assistant is currently onboarding.""" - with patch( - "homeassistant.components.onboarding.async_add_listener", - ) as mock_add_onboarding_listener: - yield mock_add_onboarding_listener - - async def test_create_dashboards_when_onboarded( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -59,40 +41,36 @@ async def test_create_dashboards_when_onboarded( assert response["result"] == [] -async def test_create_dashboards_when_not_onboarded( +@pytest.mark.parametrize("integration_frame_path", ["custom_components/my_integration"]) +@pytest.mark.usefixtures("mock_integration_frame") +async def test_hass_data_compatibility( hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - hass_storage: dict[str, Any], - mock_add_onboarding_listener, - mock_onboarding_not_done, + caplog: pytest.LogCaptureFixture, ) -> None: - """Test we automatically create dashboards when not onboarded.""" - client = await hass_ws_client(hass) + """Test compatibility for external access. + + See: + https://github.com/hacs/integration/blob/4a820e8b1b066bc54a1c9c61102038af6c030603 + /custom_components/hacs/repositories/plugin.py#L173 + """ + expected_prefix = ( + "Detected that custom integration 'my_integration' accessed lovelace_data" + ) assert await async_setup_component(hass, "lovelace", {}) - # Call onboarding listener - mock_add_onboarding_listener.mock_calls[0][1][1]() - await hass.async_block_till_done() + assert (lovelace_data := hass.data.get("lovelace")) is not None - # List dashboards - await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) - response = await client.receive_json() - assert response["success"] - assert response["result"] == [ - { - "icon": "mdi:map", - "id": "map", - "mode": "storage", - "require_admin": False, - "show_in_sidebar": True, - "title": "Map", - "url_path": "map", - } - ] + # Direct access to resources is fine + assert lovelace_data.resources is not None + assert expected_prefix not in caplog.text - # List map dashboard config - await client.send_json_auto_id({"type": "lovelace/config", "url_path": "map"}) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {"strategy": {"type": "map"}} + # Dict compatibility logs warning + with patch.object(frame, "_REPORTED_INTEGRATIONS", set()): + assert lovelace_data["resources"] is not None + assert f"{expected_prefix}['resources']" in caplog.text + + # Dict get compatibility logs warning + with patch.object(frame, "_REPORTED_INTEGRATIONS", set()): + assert lovelace_data.get("resources") is not None + assert f"{expected_prefix}.get('resources')" in caplog.text diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index b2edaa07155..bdbe6501470 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -10,9 +10,10 @@ from pylutron_caseta.smartbridge import Smartbridge import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf -from homeassistant.components.lutron_caseta import DOMAIN -import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow +from homeassistant.components.lutron_caseta import ( + DOMAIN, + config_flow as CasetaConfigFlow, +) from homeassistant.components.lutron_caseta.const import ( CONF_CA_CERTS, CONF_CERTFILE, @@ -23,6 +24,7 @@ from homeassistant.components.lutron_caseta.const import ( from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from . import ENTRY_MOCK_DATA, MockBridge @@ -421,7 +423,7 @@ async def test_zeroconf_host_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="LuTrOn-abc.local.", @@ -449,7 +451,7 @@ async def test_zeroconf_lutron_id_already_configured(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="LuTrOn-abc.local.", @@ -472,7 +474,7 @@ async def test_zeroconf_not_lutron_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="notlutron-abc.local.", @@ -500,7 +502,7 @@ async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="LuTrOn-abc.local.", diff --git a/tests/components/madvr/snapshots/test_binary_sensors.ambr b/tests/components/madvr/snapshots/test_binary_sensor.ambr similarity index 97% rename from tests/components/madvr/snapshots/test_binary_sensors.ambr rename to tests/components/madvr/snapshots/test_binary_sensor.ambr index 7fd54a7c240..7d665210a6f 100644 --- a/tests/components/madvr/snapshots/test_binary_sensors.ambr +++ b/tests/components/madvr/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/madvr/snapshots/test_remote.ambr b/tests/components/madvr/snapshots/test_remote.ambr index 1157496a93e..c90270674c8 100644 --- a/tests/components/madvr/snapshots/test_remote.ambr +++ b/tests/components/madvr/snapshots/test_remote.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/madvr/snapshots/test_sensors.ambr b/tests/components/madvr/snapshots/test_sensor.ambr similarity index 97% rename from tests/components/madvr/snapshots/test_sensors.ambr rename to tests/components/madvr/snapshots/test_sensor.ambr index 7b0dd254f77..115f6a3f5d7 100644 --- a/tests/components/madvr/snapshots/test_sensors.ambr +++ b/tests/components/madvr/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -192,6 +196,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -243,6 +248,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +300,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -348,6 +355,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -405,6 +413,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -462,6 +471,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -520,6 +530,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -583,6 +594,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -639,6 +651,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -685,6 +698,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -736,6 +750,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -789,6 +804,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -838,6 +854,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -884,6 +901,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -930,6 +948,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -982,6 +1001,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1039,6 +1059,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1097,6 +1118,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1160,6 +1182,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1216,6 +1239,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1262,6 +1286,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1313,6 +1338,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/madvr/test_binary_sensors.py b/tests/components/madvr/test_binary_sensor.py similarity index 97% rename from tests/components/madvr/test_binary_sensors.py rename to tests/components/madvr/test_binary_sensor.py index 469a3225ca0..9ddbc7b3afe 100644 --- a/tests/components/madvr/test_binary_sensors.py +++ b/tests/components/madvr/test_binary_sensor.py @@ -9,7 +9,7 @@ from syrupy import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_integration from .conftest import get_update_callback diff --git a/tests/components/madvr/test_remote.py b/tests/components/madvr/test_remote.py index 6fc507534d6..1ddbacdb6e9 100644 --- a/tests/components/madvr/test_remote.py +++ b/tests/components/madvr/test_remote.py @@ -20,7 +20,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_integration from .const import ( diff --git a/tests/components/madvr/test_sensors.py b/tests/components/madvr/test_sensor.py similarity index 98% rename from tests/components/madvr/test_sensors.py rename to tests/components/madvr/test_sensor.py index ddc01fc737a..dd1722913f2 100644 --- a/tests/components/madvr/test_sensors.py +++ b/tests/components/madvr/test_sensor.py @@ -10,7 +10,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.madvr.sensor import get_temperature from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_integration from .conftest import get_update_callback diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 9fc92cd5458..941d7523220 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -28,7 +28,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.exceptions import ServiceValidationError from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, mock_component, mock_restore_cache from tests.components.alarm_control_panel import common diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 2b401cb10a0..9bb506b935a 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( assert_setup_component, diff --git a/tests/components/mastodon/snapshots/test_init.ambr b/tests/components/mastodon/snapshots/test_init.ambr index 37fa765acea..28157b9e6eb 100644 --- a/tests/components/mastodon/snapshots/test_init.ambr +++ b/tests/components/mastodon/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/mastodon/snapshots/test_sensor.ambr b/tests/components/mastodon/snapshots/test_sensor.ambr index c8df8cdab19..22ac2671c36 100644 --- a/tests/components/mastodon/snapshots/test_sensor.ambr +++ b/tests/components/mastodon/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -108,6 +110,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/mastodon/test_config_flow.py b/tests/components/mastodon/test_config_flow.py index 33f73812348..4b022df2ca2 100644 --- a/tests/components/mastodon/test_config_flow.py +++ b/tests/components/mastodon/test_config_flow.py @@ -6,7 +6,7 @@ from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError import pytest from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -160,53 +160,3 @@ async def test_duplicate( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_import_flow( - hass: HomeAssistant, - mock_mastodon_client: AsyncMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test importing yaml config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_BASE_URL: "https://mastodon.social", - CONF_CLIENT_ID: "import_client_id", - CONF_CLIENT_SECRET: "import_client_secret", - CONF_ACCESS_TOKEN: "import_access_token", - }, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - - -@pytest.mark.parametrize( - ("exception", "error"), - [ - (MastodonNetworkError, "network_error"), - (MastodonUnauthorizedError, "unauthorized_error"), - (Exception, "unknown"), - ], -) -async def test_import_flow_abort( - hass: HomeAssistant, - mock_mastodon_client: AsyncMock, - mock_setup_entry: AsyncMock, - exception: Exception, - error: str, -) -> None: - """Test importing yaml config abort.""" - mock_mastodon_client.account_verify_credentials.side_effect = exception - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_BASE_URL: "https://mastodon.social", - CONF_CLIENT_ID: "import_client_id", - CONF_CLIENT_SECRET: "import_client_secret", - CONF_ACCESS_TOKEN: "import_access_token", - }, - ) - assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/mastodon/test_notify.py b/tests/components/mastodon/test_notify.py index ab2d7456baf..4242f88d34a 100644 --- a/tests/components/mastodon/test_notify.py +++ b/tests/components/mastodon/test_notify.py @@ -2,10 +2,13 @@ from unittest.mock import AsyncMock +from mastodon.Mastodon import MastodonAPIError +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -36,3 +39,27 @@ async def test_notify( ) assert mock_mastodon_client.status_post.assert_called_once + + +async def test_notify_failed( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the notify raising an error.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_mastodon_client.status_post.side_effect = MastodonAPIError + + with pytest.raises(HomeAssistantError, match="Unable to send message"): + await hass.services.async_call( + NOTIFY_DOMAIN, + "trwnh_mastodon_social", + { + "message": "test toot", + }, + blocking=True, + return_response=False, + ) diff --git a/tests/components/mastodon/test_services.py b/tests/components/mastodon/test_services.py new file mode 100644 index 00000000000..4dafa9a8e5b --- /dev/null +++ b/tests/components/mastodon/test_services.py @@ -0,0 +1,263 @@ +"""Tests for the Mastodon services.""" + +from unittest.mock import AsyncMock, Mock, patch + +from mastodon.Mastodon import MastodonAPIError +import pytest + +from homeassistant.components.mastodon.const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_CONTENT_WARNING, + ATTR_MEDIA, + ATTR_MEDIA_DESCRIPTION, + ATTR_STATUS, + ATTR_VISIBILITY, + DOMAIN, +) +from homeassistant.components.mastodon.services import SERVICE_POST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("payload", "kwargs"), + [ + ( + { + ATTR_STATUS: "test toot", + }, + { + "status": "test toot", + "spoiler_text": None, + "visibility": None, + "media_ids": None, + "sensitive": None, + }, + ), + ( + {ATTR_STATUS: "test toot", ATTR_VISIBILITY: "private"}, + { + "status": "test toot", + "spoiler_text": None, + "visibility": "private", + "media_ids": None, + "sensitive": None, + }, + ), + ( + { + ATTR_STATUS: "test toot", + ATTR_CONTENT_WARNING: "Spoiler", + ATTR_VISIBILITY: "private", + }, + { + "status": "test toot", + "spoiler_text": "Spoiler", + "visibility": "private", + "media_ids": None, + "sensitive": None, + }, + ), + ( + { + ATTR_STATUS: "test toot", + ATTR_CONTENT_WARNING: "Spoiler", + ATTR_MEDIA: "/image.jpg", + }, + { + "status": "test toot", + "spoiler_text": "Spoiler", + "visibility": None, + "media_ids": "1", + "sensitive": None, + }, + ), + ( + { + ATTR_STATUS: "test toot", + ATTR_CONTENT_WARNING: "Spoiler", + ATTR_MEDIA: "/image.jpg", + ATTR_MEDIA_DESCRIPTION: "A test image", + }, + { + "status": "test toot", + "spoiler_text": "Spoiler", + "visibility": None, + "media_ids": "1", + "sensitive": None, + }, + ), + ], +) +async def test_service_post( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + payload: dict[str, str], + kwargs: dict[str, str | None], +) -> None: + """Test the post service.""" + + await setup_integration(hass, mock_config_entry) + + with ( + patch.object(hass.config, "is_allowed_path", return_value=True), + patch.object(mock_mastodon_client, "media_post", return_value={"id": "1"}), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_POST, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + } + | payload, + blocking=True, + return_response=False, + ) + + mock_mastodon_client.status_post.assert_called_with(**kwargs) + + mock_mastodon_client.status_post.reset_mock() + + +@pytest.mark.parametrize( + ("payload", "kwargs"), + [ + ( + { + ATTR_STATUS: "test toot", + }, + {"status": "test toot", "spoiler_text": None, "visibility": None}, + ), + ( + { + ATTR_STATUS: "test toot", + ATTR_CONTENT_WARNING: "Spoiler", + ATTR_MEDIA: "/image.jpg", + }, + { + "status": "test toot", + "spoiler_text": "Spoiler", + "visibility": None, + "media_ids": "1", + "media_description": None, + "sensitive": None, + }, + ), + ], +) +async def test_post_service_failed( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, + payload: dict[str, str], + kwargs: dict[str, str | None], +) -> None: + """Test the post service raising an error.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + hass.config.is_allowed_path = Mock(return_value=True) + mock_mastodon_client.media_post.return_value = {"id": "1"} + + mock_mastodon_client.status_post.side_effect = MastodonAPIError + + with pytest.raises(HomeAssistantError, match="Unable to send message"): + await hass.services.async_call( + DOMAIN, + SERVICE_POST, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload, + blocking=True, + return_response=False, + ) + + +async def test_post_media_upload_failed( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the post service raising an error because media upload fails.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + payload = {"status": "test toot", "media": "/fail.jpg"} + + mock_mastodon_client.media_post.side_effect = MastodonAPIError + + with ( + patch.object(hass.config, "is_allowed_path", return_value=True), + pytest.raises(HomeAssistantError, match="Unable to upload image /fail.jpg"), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_POST, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload, + blocking=True, + return_response=False, + ) + + +async def test_post_path_not_whitelisted( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the post service raising an error because the file path is not whitelisted.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + payload = {"status": "test toot", "media": "/fail.jpg"} + + with pytest.raises( + HomeAssistantError, match="/fail.jpg is not a whitelisted directory" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_POST, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload, + blocking=True, + return_response=False, + ) + + +async def test_service_entry_availability( + hass: HomeAssistant, + mock_mastodon_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the services without valid entry.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry2 = MockConfigEntry(domain=DOMAIN) + mock_config_entry2.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + payload = {"status": "test toot"} + + with pytest.raises(ServiceValidationError, match="Mock Title is not loaded"): + await hass.services.async_call( + DOMAIN, + SERVICE_POST, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload, + blocking=True, + return_response=False, + ) + + with pytest.raises( + ServiceValidationError, match='Integration "mastodon" not found in registry' + ): + await hass.services.async_call( + DOMAIN, + SERVICE_POST, + {ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload, + blocking=True, + return_response=False, + ) diff --git a/tests/components/matrix/test_commands.py b/tests/components/matrix/test_commands.py index dabee74fdc3..ea0805b920a 100644 --- a/tests/components/matrix/test_commands.py +++ b/tests/components/matrix/test_commands.py @@ -42,9 +42,8 @@ class CommandTestParameters: Commands that are named with 'Subset' are expected not to be read from Room A. """ - if ( - self.expected_event_data_extra is None - or "Subset" in self.expected_event_data_extra["command"] + if self.expected_event_data_extra is None or ( + "Subset" in self.expected_event_data_extra["command"] and self.room_id not in SUBSET_ROOMS ): return None diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index bbafec48e10..d7429f6087d 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -104,6 +104,7 @@ async def integration_fixture( "pressure_sensor", "room_airconditioner", "silabs_dishwasher", + "silabs_laundrywasher", "smoke_detector", "switch_unit", "temperature_sensor", @@ -115,6 +116,7 @@ async def integration_fixture( "window_covering_pa_lift", "window_covering_pa_tilt", "window_covering_tilt", + "yandex_smart_socket", ] ) async def matter_devices( diff --git a/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json b/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json new file mode 100644 index 00000000000..3b1ed0043de --- /dev/null +++ b/tests/components/matter/fixtures/nodes/silabs_laundrywasher.json @@ -0,0 +1,909 @@ +{ + "node_id": 29, + "date_commissioned": "2024-10-19T19:49:36.900186", + "last_interview": "2024-10-20T09:26:38.517535", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [ + 29, 31, 40, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 60, 62, 63, 64, 65 + ], + "0/29/2": [41], + "0/29/3": [1, 2], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 4 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "Silabs", + "0/40/2": 65521, + "0/40/3": "LaundryWasher", + "0/40/4": 32773, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "", + "0/40/16": false, + "0/40/18": "DC840FF79F5DBFCE", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/21": 16973824, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65533": 3, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 21, 22, + 65528, 65529, 65531, 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/43/0": "en-US", + "0/43/1": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "0/44/0": 0, + "0/44/1": 0, + "0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7], + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/45/0": 1, + "0/45/65532": 0, + "0/45/65533": 1, + "0/45/65528": [], + "0/45/65529": [], + "0/45/65531": [0, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "p0jbsOzJRNw=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "p0jbsOzJRNw=", + "0/49/7": null, + "0/49/9": 10, + "0/49/10": 4, + "0/49/65532": 2, + "0/49/65533": 2, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "MyHome", + "1": true, + "2": null, + "3": null, + "4": "GstaSerJSho=", + "5": [], + "6": [ + "/cS6oCynAAGilSC/p+bVSg==", + "/QANuACgAAAAAAD//gDIAA==", + "/QANuACgAABL3TOUNF1NGw==", + "/oAAAAAAAAAYy1pJ6slKGg==" + ], + "7": 4 + } + ], + "0/51/1": 10, + "0/51/2": 1934, + "0/51/3": 17, + "0/51/4": 6, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/52/0": [ + { + "0": 8, + "1": "shell", + "3": 324 + }, + { + "0": 3, + "1": "UART", + "3": 127 + }, + { + "0": 2, + "1": "OT Stack", + "3": 719 + }, + { + "0": 9, + "1": "LaundryW", + "3": 767 + }, + { + "0": 12, + "1": "Bluetoot", + "3": 174 + }, + { + "0": 1, + "1": "Bluetoot", + "3": 294 + }, + { + "0": 11, + "1": "Bluetoot", + "3": 216 + }, + { + "0": 6, + "1": "Tmr Svc", + "3": 586 + }, + { + "0": 5, + "1": "IDLE", + "3": 264 + }, + { + "0": 7, + "1": "CHIP", + "3": 699 + } + ], + "0/52/1": 99808, + "0/52/2": 17592, + "0/52/3": 4294959166, + "0/52/65532": 1, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [0], + "0/52/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/53/0": 25, + "0/53/1": 5, + "0/53/2": "MyHome", + "0/53/3": 4660, + "0/53/4": 12054125955590472924, + "0/53/5": "QP0ADbgAoAAA", + "0/53/6": 0, + "0/53/7": [ + { + "0": 7100699097952925053, + "1": 23, + "2": 15360, + "3": 222256, + "4": 71507, + "5": 2, + "6": -83, + "7": -90, + "8": 56, + "9": 3, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 9656160343072683744, + "1": 16, + "2": 17408, + "3": 211448, + "4": 95936, + "5": 3, + "6": -53, + "7": -59, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 5926511551178228101, + "1": 0, + "2": 19456, + "3": 420246, + "4": 89821, + "5": 3, + "6": -57, + "7": -56, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 3029834005214616809, + "1": 8, + "2": 22528, + "3": 125241, + "4": 91286, + "5": 3, + "6": -73, + "7": -81, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 17459145101989614194, + "1": 7, + "2": 26624, + "3": 1426216, + "4": 36884, + "5": 3, + "6": -39, + "7": -39, + "8": 34, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 17503311195895696084, + "1": 30, + "2": 29696, + "3": 577028, + "4": 98083, + "5": 2, + "6": -84, + "7": -85, + "8": 65, + "9": 20, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 8241705229565301122, + "1": 19, + "2": 57344, + "3": 488092, + "4": 55364, + "5": 3, + "6": -48, + "7": -48, + "8": 1, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + } + ], + "0/53/8": [ + { + "0": 7100699097952925053, + "1": 15360, + "2": 15, + "3": 22, + "4": 1, + "5": 2, + "6": 2, + "7": 23, + "8": true, + "9": true + }, + { + "0": 9656160343072683744, + "1": 17408, + "2": 17, + "3": 19, + "4": 1, + "5": 3, + "6": 3, + "7": 16, + "8": true, + "9": true + }, + { + "0": 5926511551178228101, + "1": 19456, + "2": 19, + "3": 17, + "4": 1, + "5": 3, + "6": 3, + "7": 0, + "8": true, + "9": true + }, + { + "0": 3029834005214616809, + "1": 22528, + "2": 22, + "3": 17, + "4": 1, + "5": 3, + "6": 3, + "7": 8, + "8": true, + "9": true + }, + { + "0": 17459145101989614194, + "1": 26624, + "2": 26, + "3": 17, + "4": 1, + "5": 3, + "6": 3, + "7": 7, + "8": true, + "9": true + }, + { + "0": 17503311195895696084, + "1": 29696, + "2": 29, + "3": 26, + "4": 1, + "5": 2, + "6": 2, + "7": 30, + "8": true, + "9": true + }, + { + "0": 0, + "1": 51200, + "2": 50, + "3": 63, + "4": 0, + "5": 0, + "6": 0, + "7": 0, + "8": true, + "9": false + }, + { + "0": 8241705229565301122, + "1": 57344, + "2": 56, + "3": 17, + "4": 1, + "5": 3, + "6": 3, + "7": 19, + "8": true, + "9": true + } + ], + "0/53/9": 1348153998, + "0/53/10": 68, + "0/53/11": 49, + "0/53/12": 120, + "0/53/13": 56, + "0/53/14": 1, + "0/53/15": 0, + "0/53/16": 1, + "0/53/17": 0, + "0/53/18": 0, + "0/53/19": 1, + "0/53/20": 0, + "0/53/21": 0, + "0/53/22": 18798, + "0/53/23": 18683, + "0/53/24": 115, + "0/53/25": 18699, + "0/53/26": 18492, + "0/53/27": 115, + "0/53/28": 18814, + "0/53/29": 0, + "0/53/30": 0, + "0/53/31": 0, + "0/53/32": 0, + "0/53/33": 15745, + "0/53/34": 207, + "0/53/35": 0, + "0/53/36": 71, + "0/53/37": 0, + "0/53/38": 0, + "0/53/39": 7183, + "0/53/40": 6295, + "0/53/41": 886, + "0/53/42": 6140, + "0/53/43": 0, + "0/53/44": 0, + "0/53/45": 0, + "0/53/46": 0, + "0/53/47": 0, + "0/53/48": 0, + "0/53/49": 1041, + "0/53/50": 0, + "0/53/51": 2, + "0/53/52": 0, + "0/53/53": 0, + "0/53/54": 0, + "0/53/55": 0, + "0/53/56": 65536, + "0/53/57": 0, + "0/53/58": 0, + "0/53/59": { + "0": 672, + "1": 8335 + }, + "0/53/60": "AB//4A==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [], + "0/53/65532": 15, + "0/53/65533": 2, + "0/53/65528": [], + "0/53/65529": [0], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, + 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRHRgkBwEkCAEwCUEEJu8N93WFULw4vts483kDAExYc3VhKuaWdmpdJnF5pDcls+y34i6RfchubiU77BJq8zo9VGn6J59mVROTzKgr0DcKNQEoARgkAgE2AwQCBAEYMAQUbJ+53QmsxXf2iP0oL4td/BQFi0gwBRRT9HTfU5Nds+HA8j+/MRP+0pVyIxgwC0BFzpzN0Z0DdN+oPUwK87jzZ8amzJxWlmbnW/Q+j1Z4ziWsFy3yLAsgKYL4nOexZZSqvlEvzMhpstndmh1eGYZfGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyT62Yt4qMI+MorlmQ/Hxh2CpLetznVknlAbhvYAwTexpSxp9GnhR09SrcUhz3mOb0eZa2TylqcnPBhHJ2Ih2RTcKNQEpARgkAmAwBBRT9HTfU5Nds+HA8j+/MRP+0pVyIzAFFOMCO8Jk7ZCknJquFGPtPzJiNqsDGDALQI/Kc38hQyK7AkT7/pN4hiYW3LoWKT3NA43+ssMJoVpDcaZ989GXBQKIbHKbBEXzUQ1J8wfL7l2pL0Z8Lso9JwgY", + "254": 4 + } + ], + "0/62/1": [ + { + "1": "BIrruNo7r0gX6j6lq1dDi5zeK3jxcTavjt2o4adCCSCYtbxOakfb7C3GXqgV4LzulFSinbewmYkdqFBHqm5pxvU=", + "2": 4939, + "3": 2, + "4": 29, + "5": "", + "254": 4 + } + ], + "0/62/2": 5, + "0/62/3": 4, + "0/62/4": [ + "FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBP2G+CskBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQIrLt7Uq3S9HEe7apdzYSR+j3BLWNXSTLWD4YbrdyYLpm6xqHDV/NPARcIp4skZdtz91WwFBDfuS4jO5aVoER1sY", + "FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEE0j40vjcb6ZsmtBR/I0rB3ZIfAA8lPeWCTxG7nPSbNpepe18XwLidhFIHKmvtZWDZ3Hl3MM9NBB+LAZlCFq/edjcKNQEpARgkAmAwBBS7EfW886qYxvWeWjpA/G/CjDuwEDAFFLsR9bzzqpjG9Z5aOkD8b8KMO7AQGDALQIgQgt5asUGXO0ZyTWWKdjAmBSoJAzRMuD4Z+tQYZanQ3s0OItL07MU2In6uyXhjNBfjJlRqon780lhjTsm2Y+8Y", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEiuu42juvSBfqPqWrV0OLnN4rePFxNq+O3ajhp0IJIJi1vE5qR9vsLcZeqBXgvO6UVKKdt7CZiR2oUEeqbmnG9TcKNQEpARgkAmAwBBTjAjvCZO2QpJyarhRj7T8yYjarAzAFFOMCO8Jk7ZCknJquFGPtPzJiNqsDGDALQE7hTxTRg92QOxwA1hK3xv8DaxvxL71r6ZHcNRzug9wNnonJ+NC84SFKvKDxwcBxHYqFdIyDiDgwJNTQIBgasmIY" + ], + "0/62/5": 4, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/64/0": [ + { + "0": "room", + "1": "bedroom 2" + }, + { + "0": "orientation", + "1": "North" + }, + { + "0": "floor", + "1": "2" + }, + { + "0": "direction", + "1": "up" + } + ], + "0/64/65532": 0, + "0/64/65533": 1, + "0/64/65528": [], + "0/64/65529": [], + "0/64/65531": [0, 65528, 65529, 65531, 65532, 65533], + "0/65/0": [], + "0/65/65532": 0, + "0/65/65533": 1, + "0/65/65528": [], + "0/65/65529": [], + "0/65/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 115, + "1": 1 + } + ], + "1/29/1": [3, 29, 30, 81, 83, 86, 96], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/30/0": [], + "1/30/65532": 0, + "1/30/65533": 1, + "1/30/65528": [], + "1/30/65529": [], + "1/30/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/81/0": null, + "1/81/1": null, + "1/81/65532": null, + "1/81/65533": 2, + "1/81/65528": [1], + "1/81/65529": [0], + "1/81/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/83/0": ["Off", "Low", "Medium", "High"], + "1/83/1": 0, + "1/83/2": 0, + "1/83/3": [0, 1], + "1/83/65532": 3, + "1/83/65533": 1, + "1/83/65528": [], + "1/83/65529": [], + "1/83/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/86/0": 0, + "1/86/1": 0, + "1/86/2": 0, + "1/86/3": 0, + "1/86/4": 1, + "1/86/5": ["Cold", "Colors", "Whites"], + "1/86/65532": 2, + "1/86/65533": 1, + "1/86/65528": [], + "1/86/65529": [0], + "1/86/65531": [4, 5, 65528, 65529, 65531, 65532, 65533], + "1/96/0": ["pre-soak", "rinse", "spin"], + "1/96/1": 0, + "1/96/3": [ + { + "0": 0 + }, + { + "0": 1 + }, + { + "0": 2 + }, + { + "0": 3 + } + ], + "1/96/4": 0, + "1/96/5": { + "0": 0 + }, + "1/96/65532": 0, + "1/96/65533": 1, + "1/96/65528": [4], + "1/96/65529": [0, 1, 2, 3], + "1/96/65531": [0, 1, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 1296, + "1": 1 + } + ], + "2/29/1": [29, 144, 145, 156], + "2/29/2": [], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/144/0": 2, + "2/144/1": 3, + "2/144/2": [ + { + "0": 5, + "1": true, + "2": -50000000, + "3": 50000000, + "4": [ + { + "0": -50000000, + "1": -10000000, + "2": 5000, + "3": 2000, + "4": 3000 + }, + { + "0": -9999999, + "1": 9999999, + "2": 1000, + "3": 100, + "4": 500 + }, + { + "0": 10000000, + "1": 50000000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + }, + { + "0": 2, + "1": true, + "2": -100000, + "3": 100000, + "4": [ + { + "0": -100000, + "1": -5000, + "2": 5000, + "3": 2000, + "4": 3000 + }, + { + "0": -4999, + "1": 4999, + "2": 1000, + "3": 100, + "4": 500 + }, + { + "0": 5000, + "1": 100000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + }, + { + "0": 1, + "1": true, + "2": -500000, + "3": 500000, + "4": [ + { + "0": -500000, + "1": -100000, + "2": 5000, + "3": 2000, + "4": 3000 + }, + { + "0": -99999, + "1": 99999, + "2": 1000, + "3": 100, + "4": 500 + }, + { + "0": 100000, + "1": 500000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + } + ], + "2/144/3": [ + { + "0": 0, + "1": 0, + "2": 300, + "7": 129, + "8": 129, + "9": 129, + "10": 129 + }, + { + "0": 1, + "1": 0, + "2": 500, + "7": 129, + "8": 129, + "9": 129, + "10": 129 + }, + { + "0": 2, + "1": 0, + "2": 1000, + "7": 129, + "8": 129, + "9": 129, + "10": 129 + } + ], + "2/144/4": 120000, + "2/144/5": 0, + "2/144/6": 0, + "2/144/7": 0, + "2/144/8": 0, + "2/144/9": 0, + "2/144/10": 0, + "2/144/11": 120000, + "2/144/12": 0, + "2/144/13": 0, + "2/144/14": 60, + "2/144/15": [ + { + "0": 1, + "1": 100000 + } + ], + "2/144/16": [ + { + "0": 1, + "1": 100000 + } + ], + "2/144/17": 9800, + "2/144/18": 0, + "2/144/65532": 31, + "2/144/65533": 1, + "2/144/65528": [], + "2/144/65529": [], + "2/144/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 65528, + 65529, 65531, 65532, 65533 + ], + "2/145/0": { + "0": 14, + "1": true, + "2": 0, + "3": 1000000000000000, + "4": [ + { + "0": 0, + "1": 1000000000000000, + "2": 500, + "3": 50 + } + ] + }, + "2/145/1": { + "0": 0, + "1": 1900, + "2": 1936, + "3": 1900222, + "4": 1936790 + }, + "2/145/5": { + "0": 0, + "1": 0, + "2": 0, + "3": 0 + }, + "2/145/65532": 5, + "2/145/65533": 1, + "2/145/65528": [], + "2/145/65529": [], + "2/145/65531": [0, 1, 5, 65528, 65529, 65531, 65532, 65533], + "2/156/0": [0, 1, 2], + "2/156/1": null, + "2/156/65532": 12, + "2/156/65533": 1, + "2/156/65528": [], + "2/156/65529": [], + "2/156/65531": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/fixtures/nodes/yandex_smart_socket.json b/tests/components/matter/fixtures/nodes/yandex_smart_socket.json new file mode 100644 index 00000000000..26cdf38414f --- /dev/null +++ b/tests/components/matter/fixtures/nodes/yandex_smart_socket.json @@ -0,0 +1,278 @@ +{ + "node_id": 4, + "date_commissioned": "2024-12-05T10:54:31.635203", + "last_interview": "2024-12-05T12:16:52.038776", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 51, 52, 54, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 3 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "Yandex", + "0/40/2": 5130, + "0/40/3": "YNDX-00540", + "0/40/4": 540, + "0/40/5": "", + "0/40/6": "XX", + "0/40/7": 0, + "0/40/8": "v0.4", + "0/40/9": 18, + "0/40/10": "8.0.r13402545-18", + "0/40/15": "HP000RM000V4RW", + "0/40/17": true, + "0/40/18": "E4480D32A5480B29", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 17, 18, 19, 65528, 65529, 65531, + 65532, 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "**REDACTED**", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 30, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "**REDACTED**", + "0/49/7": null, + "0/49/65532": 1, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [ + { + "0": "WIFI_STA_DEF", + "1": true, + "2": null, + "3": null, + "4": "PAtP8Nse", + "5": ["wKgAEw=="], + "6": ["/oAAAAAAAAA+C0///vDbHg==", "/YrmoeskHZU+C0///vDbHg=="], + "7": 1 + } + ], + "0/51/1": 4, + "0/51/2": 124, + "0/51/3": 0, + "0/51/4": 1, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/52/1": 79260, + "0/52/2": 171268, + "0/52/65532": 0, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [], + "0/52/65531": [1, 2, 65528, 65529, 65531, 65532, 65533], + "0/54/0": "eJoYDvok", + "0/54/1": 4, + "0/54/2": 3, + "0/54/3": 11, + "0/54/4": -53, + "0/54/65532": 0, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [], + "0/54/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBBgkBwEkCAEwCUEEvOt9COzrjgf+b8q6FcKeKfbtqJybToVtEF0jiidbqg8FPmTIPTm1kU9hEiE6sd2N/GWSQHRoMi3YNl19h1PM3zcKNQEoARgkAgE2AwQCBAEYMAQUSu0+nQ/nOzrUNECyeBAqGPVu33YwBRS2PEiS/N109emRL3DTMaiWoWrEShgwC0DoGPCGt0HeGYnTS4TS2R7vbNhiFuuIrUQuxY5phP/UXBZosBDQTsnRTbMof18OkeO68MEcLXdIXjBJvBDaP/TsGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEz8nO5tz0gMFM5TW4YjYXGxkhr/UKHZg1rCa21StYqGd0wGaP7a5eMR+2BY20D1b11R7i6teWKnAaW+WqY0vQTjcKNQEpARgkAmAwBBS2PEiS/N109emRL3DTMaiWoWrESjAFFG//dFS5V0Y6/QdSQcC+z7idKKeJGDALQGFBsf7Ecq44e7NN8dCZIoJMUG16rmwD4ZtHtD4JPTxYabEreeblNF2ZDSgbo+A8sfz7Ci37WjznxbEj96vR8MgY", + "254": 3 + } + ], + "0/62/1": [ + { + "1": "BLB0QnDldRPfV2xt6Nd/34ja8uaWwvsLYZsF3yCdIwyB/krYZ0u1uBS0FTo7E3iqvN0cDZ7fbhw0OUsKTVZ9Y10=", + "2": 65521, + "3": 1, + "4": 4, + "5": "", + "254": 3 + } + ], + "0/62/2": 5, + "0/62/3": 4, + "0/62/4": [ + "FTABAQAkAgE3AyYU3a/iASYVn7wdXBgmBKaW1SwkBQA3BiYU3a/iASYVn7wdXBgkBwEkCAEwCUEE/OyhHiUZDgJ7iUVCKouxsZgI0DGBcK8E+vbDIHD5gfeFPNuT5sXN8aHlsEl7fZhfjbdEbIFudeJKIr5uf7+PLTcKNQEpARgkAmAwBBQtr6wAOFJ7UJLwYUKvomZh5wPaszAFFC2vrAA4UntQkvBhQq+iZmHnA9qzGDALQM5/1ziQdNcMURJqGH+j9wt7w/wPyeq8zf+u3FGgmmfhBSouJw4f+TIJLk7m/eQD0p2Q5rSDEuuwI2VBTxxeuWgY", + "FTABAQAkAgE3AycUe5hjm9Wdt4YmFewk8wUYJgTGN00tJAUANwYnFHuYY5vVnbeGJhXsJPMFGCQHASQIATAJQQR56PnGPW5p1dXhHDSVnjoah8C2+JYHzPAm5tvYgup9gf7DukH2TxxLdDEaBdD4hgQj/R8hrMYSmj8XmHQ8HhdZNwo1ASkBGCQCYDAEFN8wYcjYskj9OSQoEXkOn0QmWDrkMAUU3zBhyNiySP05JCgReQ6fRCZYOuQYMAtA+j7ir4H1KYIxAe49jhZr/Gg7pDUKtIcYyUVJD0g9egIYHShM1y1j3BsOQTBX6mnLPp4FS4AtNsUgaM+XPKSFSxg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEsHRCcOV1E99XbG3o13/fiNry5pbC+wthmwXfIJ0jDIH+SthnS7W4FLQVOjsTeKq83RwNnt9uHDQ5SwpNVn1jXTcKNQEpARgkAmAwBBRv/3RUuVdGOv0HUkHAvs+4nSiniTAFFG//dFS5V0Y6/QdSQcC+z7idKKeJGDALQKrvVhoinxo07C2nI/zakt4xUZKgab6DVI4mBXYoPQXaZM8jmEqWboPnLBUGbr9UAnqEc9yARHwlC77eXN1BCdUY", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEMmvMdDf/h+u7fawdjIe6gXEeWuszCShR8ulsHMLnJYTMHVrkztOcj4cHw6haH/q909aVmL3xLlbEC2lZtmZClDcKNQEpARgkAmAwBBRoZjEcSXeh6IFBtW0A2OilJBdeYjAFFGhmMRxJd6HogUG1bQDY6KUkF15iGDALQJm5+/SkVrR4iBpGVqZZGOH+DpS+cQYqceN1+JSnDFwxJe+khYxFifMohSQ5NLlTiJQTZWYpqMKMZHT36pWWADUY" + ], + "0/62/5": 3, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/6/0": true, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": 0, + "1/6/65532": 1, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ], + "1/29/0": [ + { + "0": 266, + "1": 2 + } + ], + "1/29/1": [3, 4, 6, 29, 2820, 336264194, 336264195], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/2820/0": 9, + "1/2820/1285": 2170, + "1/2820/1288": 592, + "1/2820/1291": 560, + "1/2820/1536": 1, + "1/2820/1537": 10, + "1/2820/1538": 1, + "1/2820/1539": 1000, + "1/2820/1540": 1, + "1/2820/1541": 8, + "1/2820/2049": 2530, + "1/2820/2050": 16300, + "1/2820/65532": 0, + "1/2820/65533": 3, + "1/2820/65528": [], + "1/2820/65529": [], + "1/2820/65531": [ + 0, 1285, 1288, 1291, 1536, 1537, 1538, 1539, 1540, 1541, 2049, 2050, + 65528, 65529, 65531, 65532, 65533 + ], + "1/336264194/336199680": 44, + "1/336264194/336199681": 0, + "1/336264194/336199682": 0, + "1/336264194/336199698": 70, + "1/336264194/65532": 0, + "1/336264194/65533": 1, + "1/336264194/65528": [], + "1/336264194/65529": [], + "1/336264194/65531": [ + 65528, 65529, 65531, 336199680, 336199681, 336199682, 336199698, 65532, + 65533 + ], + "1/336264195/336199680": 0, + "1/336264195/65532": 0, + "1/336264195/65533": 1, + "1/336264195/65528": [], + "1/336264195/65529": [], + "1/336264195/65531": [65528, 65529, 65531, 336199680, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index 82dcc166f13..c8de905d03f 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -382,6 +390,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -429,6 +438,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -476,6 +486,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -523,6 +534,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -569,6 +581,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -616,6 +629,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index 10792b58d28..448136eeed2 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -1,239 +1,4 @@ # serializer version: 1 -# name: test_buttons[air_purifier][button.air_purifier_identify_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.air_purifier_identify_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (1)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Air Purifier Identify (1)', - }), - 'context': , - 'entity_id': 'button.air_purifier_identify_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.air_purifier_identify_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (2)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Air Purifier Identify (2)', - }), - 'context': , - 'entity_id': 'button.air_purifier_identify_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.air_purifier_identify_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (3)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-3-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Air Purifier Identify (3)', - }), - 'context': , - 'entity_id': 'button.air_purifier_identify_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_4-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.air_purifier_identify_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (4)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-4-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_4-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Air Purifier Identify (4)', - }), - 'context': , - 'entity_id': 'button.air_purifier_identify_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_5-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.air_purifier_identify_5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (5)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-5-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[air_purifier][button.air_purifier_identify_5-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Air Purifier Identify (5)', - }), - 'context': , - 'entity_id': 'button.air_purifier_identify_5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[air_purifier][button.air_purifier_reset_filter_condition-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -241,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -287,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -326,53 +93,6 @@ 'state': 'unknown', }) # --- -# name: test_buttons[air_quality_sensor][button.lightfi_aq1_air_quality_sensor_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.lightfi_aq1_air_quality_sensor_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[air_quality_sensor][button.lightfi_aq1_air_quality_sensor_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'lightfi-aq1-air-quality-sensor Identify', - }), - 'context': , - 'entity_id': 'button.lightfi_aq1_air_quality_sensor_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[color_temperature_light][button.mock_color_temperature_light_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -380,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -402,7 +123,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -427,6 +148,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -449,7 +171,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -467,100 +189,6 @@ 'state': 'unknown', }) # --- -# name: test_buttons[door_lock][button.mock_door_lock_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_door_lock_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[door_lock][button.mock_door_lock_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Door Lock Identify', - }), - 'context': , - 'entity_id': 'button.mock_door_lock_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[door_lock_with_unbolt][button.mock_door_lock_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_door_lock_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[door_lock_with_unbolt][button.mock_door_lock_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Door Lock Identify', - }), - 'context': , - 'entity_id': 'button.mock_door_lock_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[eve_contact_sensor][button.eve_door_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -568,6 +196,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -590,7 +219,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -615,6 +244,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -637,7 +267,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -662,6 +292,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -684,7 +315,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -709,6 +340,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -731,7 +363,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -756,6 +388,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -778,7 +411,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -803,6 +436,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -825,7 +459,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -850,6 +484,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -872,7 +507,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -897,6 +532,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -919,7 +555,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -937,335 +573,6 @@ 'state': 'unknown', }) # --- -# name: test_buttons[flow_sensor][button.mock_flow_sensor_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_flow_sensor_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[flow_sensor][button.mock_flow_sensor_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Flow Sensor Identify', - }), - 'context': , - 'entity_id': 'button.mock_flow_sensor_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[generic_switch][button.mock_generic_switch_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_generic_switch_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[generic_switch][button.mock_generic_switch_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Generic Switch Identify', - }), - 'context': , - 'entity_id': 'button.mock_generic_switch_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[generic_switch_multi][button.mock_generic_switch_fancy_button-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_generic_switch_fancy_button', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Fancy Button', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-2-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[generic_switch_multi][button.mock_generic_switch_fancy_button-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Generic Switch Fancy Button', - }), - 'context': , - 'entity_id': 'button.mock_generic_switch_fancy_button', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[generic_switch_multi][button.mock_generic_switch_identify_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_generic_switch_identify_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (1)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[generic_switch_multi][button.mock_generic_switch_identify_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Generic Switch Identify (1)', - }), - 'context': , - 'entity_id': 'button.mock_generic_switch_identify_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[humidity_sensor][button.mock_humidity_sensor_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_humidity_sensor_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[humidity_sensor][button.mock_humidity_sensor_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Humidity Sensor Identify', - }), - 'context': , - 'entity_id': 'button.mock_humidity_sensor_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[light_sensor][button.mock_light_sensor_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_light_sensor_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[light_sensor][button.mock_light_sensor_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Light Sensor Identify', - }), - 'context': , - 'entity_id': 'button.mock_light_sensor_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[microwave_oven][button.microwave_oven_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.microwave_oven_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[microwave_oven][button.microwave_oven_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Microwave Oven Identify', - }), - 'context': , - 'entity_id': 'button.microwave_oven_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[microwave_oven][button.microwave_oven_pause-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1273,6 +580,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1319,6 +627,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1365,6 +674,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1411,6 +721,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1457,6 +768,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1479,7 +791,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1504,6 +816,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1526,7 +839,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1551,6 +864,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1573,7 +887,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1598,6 +912,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1620,7 +935,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-2-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-2-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1645,6 +960,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1667,7 +983,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1692,6 +1008,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1714,7 +1031,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1739,6 +1056,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1761,7 +1079,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1786,6 +1104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1808,7 +1127,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -1826,147 +1145,6 @@ 'state': 'unknown', }) # --- -# name: test_buttons[onoff_light][button.mock_onoff_light_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_onoff_light_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[onoff_light][button.mock_onoff_light_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock OnOff Light Identify', - }), - 'context': , - 'entity_id': 'button.mock_onoff_light_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[onoff_light_alt_name][button.mock_onoff_light_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_onoff_light_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[onoff_light_alt_name][button.mock_onoff_light_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock OnOff Light Identify', - }), - 'context': , - 'entity_id': 'button.mock_onoff_light_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[onoff_light_no_name][button.mock_light_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_light_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[onoff_light_no_name][button.mock_light_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Light Identify', - }), - 'context': , - 'entity_id': 'button.mock_light_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[onoff_light_with_levelcontrol_present][button.d215s_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1974,6 +1152,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1996,7 +1175,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2014,147 +1193,6 @@ 'state': 'unknown', }) # --- -# name: test_buttons[pressure_sensor][button.mock_pressure_sensor_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_pressure_sensor_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[pressure_sensor][button.mock_pressure_sensor_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Pressure Sensor Identify', - }), - 'context': , - 'entity_id': 'button.mock_pressure_sensor_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[room_airconditioner][button.room_airconditioner_identify_1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.room_airconditioner_identify_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (1)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[room_airconditioner][button.room_airconditioner_identify_1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Room AirConditioner Identify (1)', - }), - 'context': , - 'entity_id': 'button.room_airconditioner_identify_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[room_airconditioner][button.room_airconditioner_identify_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.room_airconditioner_identify_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify (2)', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-2-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[room_airconditioner][button.room_airconditioner_identify_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Room AirConditioner Identify (2)', - }), - 'context': , - 'entity_id': 'button.room_airconditioner_identify_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[silabs_dishwasher][button.dishwasher_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2162,6 +1200,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2184,7 +1223,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2209,6 +1248,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2255,6 +1295,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2301,6 +1342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2340,6 +1382,242 @@ 'state': 'unknown', }) # --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_identify-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.laundrywasher_identify', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Identify', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_identify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'identify', + 'friendly_name': 'LaundryWasher Identify', + }), + 'context': , + 'entity_id': 'button.laundrywasher_identify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_pause-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.laundrywasher_pause', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pause', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pause', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStatePauseButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_pause-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LaundryWasher Pause', + }), + 'context': , + 'entity_id': 'button.laundrywasher_pause', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_resume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.laundrywasher_resume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Resume', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'resume', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateResumeButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_resume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LaundryWasher Resume', + }), + 'context': , + 'entity_id': 'button.laundrywasher_resume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.laundrywasher_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateStartButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LaundryWasher Start', + }), + 'context': , + 'entity_id': 'button.laundrywasher_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.laundrywasher_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateStopButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[silabs_laundrywasher][button.laundrywasher_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LaundryWasher Stop', + }), + 'context': , + 'entity_id': 'button.laundrywasher_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[smoke_detector][button.smoke_sensor_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2347,6 +1625,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2369,7 +1648,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2394,6 +1673,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2416,7 +1696,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2441,6 +1721,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2463,7 +1744,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-0-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2488,6 +1769,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2510,7 +1792,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2528,147 +1810,6 @@ 'state': 'unknown', }) # --- -# name: test_buttons[valve][button.valve_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.valve_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[valve][button.valve_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Valve Identify', - }), - 'context': , - 'entity_id': 'button.valve_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[window_covering_full][button.mock_full_window_covering_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_full_window_covering_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[window_covering_full][button.mock_full_window_covering_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Full Window Covering Identify', - }), - 'context': , - 'entity_id': 'button.mock_full_window_covering_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[window_covering_lift][button.mock_lift_window_covering_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_lift_window_covering_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[window_covering_lift][button.mock_lift_window_covering_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Lift Window Covering Identify', - }), - 'context': , - 'entity_id': 'button.mock_lift_window_covering_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_buttons[window_covering_pa_lift][button.longan_link_wncv_da01_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2676,6 +1817,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2698,7 +1840,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- @@ -2716,19 +1858,20 @@ 'state': 'unknown', }) # --- -# name: test_buttons[window_covering_pa_tilt][button.mock_pa_tilt_window_covering_identify-entry] +# name: test_buttons[yandex_smart_socket][button.yndx_00540_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'button', 'entity_category': , - 'entity_id': 'button.mock_pa_tilt_window_covering_identify', + 'entity_id': 'button.yndx_00540_identify', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2745,65 +1888,18 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-IdentifyButton-3-65529', + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-IdentifyButton-3-1', 'unit_of_measurement': None, }) # --- -# name: test_buttons[window_covering_pa_tilt][button.mock_pa_tilt_window_covering_identify-state] +# name: test_buttons[yandex_smart_socket][button.yndx_00540_identify-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'identify', - 'friendly_name': 'Mock PA Tilt Window Covering Identify', + 'friendly_name': 'YNDX-00540 Identify', }), 'context': , - 'entity_id': 'button.mock_pa_tilt_window_covering_identify', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_buttons[window_covering_tilt][button.mock_tilt_window_covering_identify-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'button', - 'entity_category': , - 'entity_id': 'button.mock_tilt_window_covering_identify', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Identify', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-IdentifyButton-3-65529', - 'unit_of_measurement': None, - }) -# --- -# name: test_buttons[window_covering_tilt][button.mock_tilt_window_covering_identify-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'identify', - 'friendly_name': 'Mock Tilt Window Covering Identify', - }), - 'context': , - 'entity_id': 'button.mock_tilt_window_covering_identify', + 'entity_id': 'button.yndx_00540_identify', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/matter/snapshots/test_climate.ambr b/tests/components/matter/snapshots/test_climate.ambr index 25f5ca06f62..8aeb1aaafdd 100644 --- a/tests/components/matter/snapshots/test_climate.ambr +++ b/tests/components/matter/snapshots/test_climate.ambr @@ -13,6 +13,7 @@ 'min_temp': 5.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -75,6 +76,7 @@ 'min_temp': 10.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -141,6 +143,7 @@ 'min_temp': 16.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -209,6 +212,7 @@ 'min_temp': 7, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_cover.ambr b/tests/components/matter/snapshots/test_cover.ambr index 7d036d35983..c83dcf63c6b 100644 --- a/tests/components/matter/snapshots/test_cover.ambr +++ b/tests/components/matter/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -104,6 +106,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -153,6 +156,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -202,6 +206,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_event.ambr b/tests/components/matter/snapshots/test_event.ambr index 031e8e9d24f..b0ddfaed8bf 100644 --- a/tests/components/matter/snapshots/test_event.ambr +++ b/tests/components/matter/snapshots/test_event.ambr @@ -13,6 +13,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -74,6 +75,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -135,6 +137,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -199,6 +202,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -266,6 +270,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -333,6 +338,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_fan.ambr b/tests/components/matter/snapshots/test_fan.ambr index 7f1fe7d42db..e4dc14967e5 100644 --- a/tests/components/matter/snapshots/test_fan.ambr +++ b/tests/components/matter/snapshots/test_fan.ambr @@ -15,6 +15,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -84,6 +85,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +152,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -214,6 +217,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_light.ambr b/tests/components/matter/snapshots/test_light.ambr index eff5820d27d..a56f8f891e9 100644 --- a/tests/components/matter/snapshots/test_light.ambr +++ b/tests/components/matter/snapshots/test_light.ambr @@ -14,6 +14,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -89,6 +90,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -145,6 +147,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -207,6 +210,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -284,6 +288,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -346,6 +351,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -413,6 +419,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -474,6 +481,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -547,6 +555,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -614,6 +623,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_lock.ambr b/tests/components/matter/snapshots/test_lock.ambr index bf34ac267d7..10ba84dd49b 100644 --- a/tests/components/matter/snapshots/test_lock.ambr +++ b/tests/components/matter/snapshots/test_lock.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index 9d51bb92e51..dc35f6f2a69 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -122,6 +124,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -177,6 +180,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -233,6 +237,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -289,6 +294,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -344,6 +350,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -388,6 +395,64 @@ 'state': '1.0', }) # --- +# name: test_numbers[eve_thermo][number.eve_thermo_temperature_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 25, + 'min': -25, + 'mode': , + 'step': 0.5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.eve_thermo_temperature_offset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature offset', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_offset', + 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-EveTemperatureOffset-513-16', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[eve_thermo][number.eve_thermo_temperature_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Eve Thermo Temperature offset', + 'max': 25, + 'min': -25, + 'mode': , + 'step': 0.5, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.eve_thermo_temperature_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_numbers[eve_weather_sensor][number.eve_weather_altitude_above_sea_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -400,6 +465,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -457,6 +523,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -512,6 +579,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -568,6 +636,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -623,6 +692,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -678,6 +748,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -734,6 +805,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -790,6 +862,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -846,6 +919,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -901,6 +975,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -957,6 +1032,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1013,6 +1089,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1069,6 +1146,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1124,6 +1202,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1180,6 +1259,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1236,6 +1316,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1292,6 +1373,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1347,6 +1429,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1403,6 +1486,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1459,6 +1543,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1514,6 +1599,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 663b0cdaf51..772ee297e13 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -70,6 +71,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -138,6 +140,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -206,6 +209,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -265,6 +269,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -324,6 +329,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -383,6 +389,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -442,6 +449,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -501,6 +509,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -546,6 +555,62 @@ 'state': 'previous', }) # --- +# name: test_selects[eve_thermo][select.eve_thermo_temperature_display_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Celsius', + 'Fahrenheit', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.eve_thermo_temperature_display_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature display mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_display_mode', + 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[eve_thermo][select.eve_thermo_temperature_display_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Eve Thermo Temperature display mode', + 'options': list([ + 'Celsius', + 'Fahrenheit', + ]), + }), + 'context': , + 'entity_id': 'select.eve_thermo_temperature_display_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Celsius', + }) +# --- # name: test_selects[extended_color_light][select.mock_extended_color_light_lighting-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -559,6 +624,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -617,6 +683,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -674,6 +741,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -742,6 +810,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -821,6 +890,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -889,6 +959,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -948,6 +1019,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1005,6 +1077,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1060,6 +1133,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1120,6 +1194,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1182,6 +1257,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1241,6 +1317,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1300,6 +1377,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1359,6 +1437,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1418,6 +1497,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1463,22 +1543,25 @@ 'state': 'previous', }) # --- -# name: test_selects[silabs_dishwasher][select.dishwasher_mode-entry] +# name: test_selects[silabs_laundrywasher][select.laundrywasher_number_of_rinses-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ 'options': list([ + 'off', + 'normal', ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'select', 'entity_category': None, - 'entity_id': 'select.dishwasher_mode', + 'entity_id': 'select.laundrywasher_number_of_rinses', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1490,28 +1573,148 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Mode', + 'original_name': 'Number of rinses', 'platform': 'matter', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'mode', - 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-MatterDishwasherMode-89-1', + 'translation_key': 'laundry_washer_number_of_rinses', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-MatterLaundryWasherNumberOfRinses-83-2', 'unit_of_measurement': None, }) # --- -# name: test_selects[silabs_dishwasher][select.dishwasher_mode-state] +# name: test_selects[silabs_laundrywasher][select.laundrywasher_number_of_rinses-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Dishwasher Mode', + 'friendly_name': 'LaundryWasher Number of rinses', 'options': list([ + 'off', + 'normal', ]), }), 'context': , - 'entity_id': 'select.dishwasher_mode', + 'entity_id': 'select.laundrywasher_number_of_rinses', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'off', + }) +# --- +# name: test_selects[silabs_laundrywasher][select.laundrywasher_spin_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Off', + 'Low', + 'Medium', + 'High', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.laundrywasher_spin_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Spin speed', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'laundry_washer_spin_speed', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-LaundryWasherControlsSpinSpeed-83-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[silabs_laundrywasher][select.laundrywasher_spin_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LaundryWasher Spin speed', + 'options': list([ + 'Off', + 'Low', + 'Medium', + 'High', + ]), + }), + 'context': , + 'entity_id': 'select.laundrywasher_spin_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Off', + }) +# --- +# name: test_selects[silabs_laundrywasher][select.laundrywasher_temperature_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Cold', + 'Colors', + 'Whites', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.laundrywasher_temperature_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_level', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-TemperatureControlSelectedTemperatureLevel-86-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[silabs_laundrywasher][select.laundrywasher_temperature_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LaundryWasher Temperature level', + 'options': list([ + 'Cold', + 'Colors', + 'Whites', + ]), + }), + 'context': , + 'entity_id': 'select.laundrywasher_temperature_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Colors', }) # --- # name: test_selects[switch_unit][select.mock_switchunit_power_on_behavior_on_startup-entry] @@ -1528,6 +1731,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1573,6 +1777,62 @@ 'state': 'previous', }) # --- +# name: test_selects[thermostat][select.longan_link_hvac_temperature_display_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Celsius', + 'Fahrenheit', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.longan_link_hvac_temperature_display_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature display mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_display_mode', + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[thermostat][select.longan_link_hvac_temperature_display_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Longan link HVAC Temperature display mode', + 'options': list([ + 'Celsius', + 'Fahrenheit', + ]), + }), + 'context': , + 'entity_id': 'select.longan_link_hvac_temperature_display_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Celsius', + }) +# --- # name: test_selects[vacuum_cleaner][select.mock_vacuum_clean_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1588,6 +1848,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1634,3 +1895,63 @@ 'state': 'Quick', }) # --- +# name: test_selects[yandex_smart_socket][select.yndx_00540_power_on_behavior_on_startup-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'on', + 'off', + 'toggle', + 'previous', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.yndx_00540_power_on_behavior_on_startup', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power-on behavior on startup', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'startup_on_off', + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[yandex_smart_socket][select.yndx_00540_power_on_behavior_on_startup-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'YNDX-00540 Power-on behavior on startup', + 'options': list([ + 'on', + 'off', + 'toggle', + 'previous', + ]), + }), + 'context': , + 'entity_id': 'select.yndx_00540_power_on_behavior_on_startup', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 60a3d33a130..9caa84bbf96 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -122,6 +124,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -173,6 +176,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -224,6 +228,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -274,6 +279,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -325,6 +331,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -376,6 +383,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -427,6 +435,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -478,6 +487,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -529,6 +539,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -580,6 +591,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -622,6 +634,58 @@ 'state': '20.0', }) # --- +# name: test_sensors[air_purifier][sensor.air_purifier_temperature_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.air_purifier_temperature_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-5-ThermostatLocalTemperature-513-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[air_purifier][sensor.air_purifier_temperature_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Air Purifier Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.air_purifier_temperature_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- # name: test_sensors[air_purifier][sensor.air_purifier_vocs-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -631,6 +695,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -689,6 +754,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -746,6 +812,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -797,6 +864,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -848,6 +916,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -899,6 +968,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -950,6 +1020,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1001,6 +1072,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1052,6 +1124,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1103,6 +1176,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1145,98 +1219,6 @@ 'state': '189.0', }) # --- -# name: test_sensors[door_lock][sensor.mock_door_lock_battery_type-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_door_lock_battery_type', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery type', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'battery_replacement_description', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSourceBatReplacementDescription-47-19', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_battery_type-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Battery type', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_battery_type', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '', - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_battery_type-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_door_lock_battery_type', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery type', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'battery_replacement_description', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSourceBatReplacementDescription-47-19', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_battery_type-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Battery type', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_battery_type', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '', - }) -# --- # name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1246,6 +1228,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1288,52 +1271,6 @@ 'state': '100', }) # --- -# name: test_sensors[eve_contact_sensor][sensor.eve_door_battery_type-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.eve_door_battery_type', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery type', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'battery_replacement_description', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSourceBatReplacementDescription-47-19', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[eve_contact_sensor][sensor.eve_door_battery_type-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Eve Door Battery type', - }), - 'context': , - 'entity_id': 'sensor.eve_door_battery_type', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '', - }) -# --- # name: test_sensors[eve_contact_sensor][sensor.eve_door_voltage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1343,6 +1280,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1357,6 +1295,9 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -1394,6 +1335,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1448,6 +1390,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1502,6 +1445,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1556,6 +1500,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1610,6 +1555,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1627,6 +1573,9 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -1664,6 +1613,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1721,6 +1671,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1738,6 +1689,9 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -1775,6 +1729,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1792,6 +1747,9 @@ 'sensor': dict({ 'suggested_display_precision': 0, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -1829,6 +1787,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1871,19 +1830,22 @@ 'state': '100', }) # --- -# name: test_sensors[eve_thermo][sensor.eve_thermo_battery_type-entry] +# name: test_sensors[eve_thermo][sensor.eve_thermo_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.eve_thermo_battery_type', + 'entity_category': None, + 'entity_id': 'sensor.eve_thermo_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1893,28 +1855,31 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery type', + 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'battery_replacement_description', - 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-0-PowerSourceBatReplacementDescription-47-19', - 'unit_of_measurement': None, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-ThermostatLocalTemperature-513-0', + 'unit_of_measurement': , }) # --- -# name: test_sensors[eve_thermo][sensor.eve_thermo_battery_type-state] +# name: test_sensors[eve_thermo][sensor.eve_thermo_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Eve Thermo Battery type', + 'device_class': 'temperature', + 'friendly_name': 'Eve Thermo Temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.eve_thermo_battery_type', + 'entity_id': 'sensor.eve_thermo_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '', + 'state': '21.0', }) # --- # name: test_sensors[eve_thermo][sensor.eve_thermo_valve_position-entry] @@ -1924,6 +1889,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1973,6 +1939,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1987,6 +1954,9 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -2012,7 +1982,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3.05', + 'state': '3.050', }) # --- # name: test_sensors[eve_weather_sensor][sensor.eve_weather_battery-entry] @@ -2024,6 +1994,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2066,52 +2037,6 @@ 'state': '100', }) # --- -# name: test_sensors[eve_weather_sensor][sensor.eve_weather_battery_type-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.eve_weather_battery_type', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Battery type', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'battery_replacement_description', - 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-0-PowerSourceBatReplacementDescription-47-19', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[eve_weather_sensor][sensor.eve_weather_battery_type-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Eve Weather Battery type', - }), - 'context': , - 'entity_id': 'sensor.eve_weather_battery_type', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '', - }) -# --- # name: test_sensors[eve_weather_sensor][sensor.eve_weather_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2121,6 +2046,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2172,6 +2098,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2226,6 +2153,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2277,6 +2205,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2291,6 +2220,9 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -2328,6 +2260,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2378,6 +2311,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2429,6 +2363,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2485,6 +2420,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2540,6 +2476,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2591,6 +2528,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2642,6 +2580,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2659,6 +2598,9 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -2696,6 +2638,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2759,6 +2702,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2815,6 +2759,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2832,6 +2777,9 @@ 'sensor': dict({ 'suggested_display_precision': 2, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -2869,6 +2817,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2886,6 +2835,9 @@ 'sensor': dict({ 'suggested_display_precision': 0, }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -2914,6 +2866,358 @@ 'state': '120.0', }) # --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.laundrywasher_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'LaundryWasher Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pre-soak', + 'rinse', + 'spin', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.laundrywasher_current_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateCurrentPhase-96-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_current_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'LaundryWasher Current phase', + 'options': list([ + 'pre-soak', + 'rinse', + 'spin', + ]), + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_current_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pre-soak', + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.laundrywasher_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalEnergyMeasurementCumulativeEnergyImported-145-1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'LaundryWasher Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_operational_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'stopped', + 'running', + 'paused', + 'error', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.laundrywasher_operational_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operational state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'operational_state', + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalState-96-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_operational_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'LaundryWasher Operational state', + 'options': list([ + 'stopped', + 'running', + 'paused', + 'error', + ]), + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_operational_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stopped', + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.laundrywasher_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementWatt-144-8', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'LaundryWasher Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.laundrywasher_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[silabs_laundrywasher][sensor.laundrywasher_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'LaundryWasher Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.laundrywasher_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120.0', + }) +# --- # name: test_sensors[smoke_detector][sensor.smoke_sensor_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2923,6 +3227,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2972,6 +3277,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3020,6 +3326,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3034,6 +3341,9 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), 'original_device_class': , 'original_icon': None, @@ -3059,7 +3369,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': '0.000', }) # --- # name: test_sensors[temperature_sensor][sensor.mock_temperature_sensor_temperature-entry] @@ -3071,6 +3381,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3113,3 +3424,287 @@ 'state': '21.0', }) # --- +# name: test_sensors[thermostat][sensor.longan_link_hvac_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.longan_link_hvac_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ThermostatLocalTemperature-513-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[thermostat][sensor.longan_link_hvac_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Longan link HVAC Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.longan_link_hvac_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.3', + }) +# --- +# name: test_sensors[vacuum_cleaner][sensor.mock_vacuum_operational_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'stopped', + 'running', + 'paused', + 'error', + 'seeking_charger', + 'charging', + 'docked', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_vacuum_operational_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operational state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'operational_state', + 'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-RvcOperationalState-97-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[vacuum_cleaner][sensor.mock_vacuum_operational_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Mock Vacuum Operational state', + 'options': list([ + 'stopped', + 'running', + 'paused', + 'error', + 'seeking_charger', + 'charging', + 'docked', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_vacuum_operational_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[yandex_smart_socket][sensor.yndx_00540_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.yndx_00540_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ElectricalMeasurementRmsCurrent-2820-1288', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[yandex_smart_socket][sensor.yndx_00540_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'YNDX-00540 Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.yndx_00540_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.59', + }) +# --- +# name: test_sensors[yandex_smart_socket][sensor.yndx_00540_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.yndx_00540_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ElectricalMeasurementActivePower-2820-1291', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[yandex_smart_socket][sensor.yndx_00540_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'YNDX-00540 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.yndx_00540_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '70.0', + }) +# --- +# name: test_sensors[yandex_smart_socket][sensor.yndx_00540_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.yndx_00540_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ElectricalMeasurementRmsVoltage-2820-1285', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[yandex_smart_socket][sensor.yndx_00540_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'YNDX-00540 Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.yndx_00540_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '217.0', + }) +# --- diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index 9396dccd245..ebf43117846 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -187,6 +191,53 @@ 'state': 'off', }) # --- +# name: test_switches[eve_thermo][switch.eve_thermo_child_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.eve_thermo_child_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Child lock', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'child_lock', + 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-EveTrvChildLock-516-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[eve_thermo][switch.eve_thermo_child_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Eve Thermo Child lock', + }), + 'context': , + 'entity_id': 'switch.eve_thermo_child_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switches[on_off_plugin_unit][switch.mock_onoffpluginunit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -194,6 +245,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +293,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +341,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +389,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -375,3 +430,51 @@ 'state': 'on', }) # --- +# name: test_switches[yandex_smart_socket][switch.yndx_00540-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.yndx_00540', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterPlug-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[yandex_smart_socket][switch.yndx_00540-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'YNDX-00540', + }), + 'context': , + 'entity_id': 'switch.yndx_00540', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/matter/snapshots/test_vacuum.ambr b/tests/components/matter/snapshots/test_vacuum.ambr index 9e6b52ed572..0703a1af4c7 100644 --- a/tests/components/matter/snapshots/test_vacuum.ambr +++ b/tests/components/matter/snapshots/test_vacuum.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/snapshots/test_valve.ambr b/tests/components/matter/snapshots/test_valve.ambr index 98634635476..99da4c2d0f6 100644 --- a/tests/components/matter/snapshots/test_valve.ambr +++ b/tests/components/matter/snapshots/test_valve.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index eed776c132e..24243fa2038 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -14,10 +14,10 @@ import pytest from homeassistant import config_entries from homeassistant.components.matter.const import ADDON_SLUG, DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry diff --git a/tests/components/matter/test_lock.py b/tests/components/matter/test_lock.py index 7bcfd381d6c..bb03b296fc6 100644 --- a/tests/components/matter/test_lock.py +++ b/tests/components/matter/test_lock.py @@ -11,7 +11,7 @@ from homeassistant.components.lock import LockEntityFeature, LockState from homeassistant.const import ATTR_CODE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import ( set_node_attribute, diff --git a/tests/components/matter/test_number.py b/tests/components/matter/test_number.py index 86e1fbbf419..2a4eea1c324 100644 --- a/tests/components/matter/test_number.py +++ b/tests/components/matter/test_number.py @@ -4,12 +4,14 @@ from unittest.mock import MagicMock, call from matter_server.client.models.node import MatterNode from matter_server.common import custom_clusters +from matter_server.common.errors import MatterError from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import ( @@ -97,3 +99,25 @@ async def test_eve_weather_sensor_altitude( ), value=500, ) + + +@pytest.mark.parametrize("node_fixture", ["dimmable_light"]) +async def test_matter_exception_on_write_attribute( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test if a MatterError gets converted to HomeAssistantError by using a dimmable_light fixture.""" + state = hass.states.get("number.mock_dimmable_light_on_level") + assert state + matter_client.write_attribute.side_effect = MatterError("Boom") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "number", + "set_value", + { + "entity_id": "number.mock_dimmable_light_on_level", + "value": 500, + }, + blocking=True, + ) diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index ffe996fd840..2403b4b1623 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode +from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest from syrupy import SnapshotAssertion @@ -103,3 +104,97 @@ async def test_attribute_select_entities( await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state.state == "unknown" + + +@pytest.mark.parametrize("node_fixture", ["silabs_laundrywasher"]) +async def test_list_select_entities( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test ListSelect entities are discovered and working from a laundrywasher fixture.""" + state = hass.states.get("select.laundrywasher_temperature_level") + assert state + assert state.state == "Colors" + assert state.attributes["options"] == ["Cold", "Colors", "Whites"] + # Change temperature_level + set_node_attribute(matter_node, 1, 86, 4, 0) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.laundrywasher_temperature_level") + assert state.state == "Cold" + # test select option + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": "select.laundrywasher_temperature_level", + "option": "Whites", + }, + blocking=True, + ) + assert matter_client.send_device_command.call_count == 1 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=1, + command=clusters.TemperatureControl.Commands.SetTemperature( + targetTemperatureLevel=2 + ), + ) + # test that an invalid value (e.g. 253) leads to an unknown state + set_node_attribute(matter_node, 1, 86, 4, 253) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.laundrywasher_temperature_level") + assert state.state == "unknown" + + # SpinSpeedCurrent + matter_client.write_attribute.reset_mock() + state = hass.states.get("select.laundrywasher_spin_speed") + assert state + assert state.state == "Off" + assert state.attributes["options"] == ["Off", "Low", "Medium", "High"] + set_node_attribute(matter_node, 1, 83, 1, 3) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.laundrywasher_spin_speed") + assert state.state == "High" + # test select option + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": "select.laundrywasher_spin_speed", + "option": "High", + }, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=matter_node.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.LaundryWasherControls.Attributes.SpinSpeedCurrent, + ), + value=3, + ) + # test that an invalid value (e.g. 253) leads to an unknown state + set_node_attribute(matter_node, 1, 83, 1, 253) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.laundrywasher_spin_speed") + assert state.state == "unknown" + + +@pytest.mark.parametrize("node_fixture", ["silabs_laundrywasher"]) +async def test_map_select_entities( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test MatterMapSelectEntity entities are discovered and working from a laundrywasher fixture.""" + # NumberOfRinses + state = hass.states.get("select.laundrywasher_number_of_rinses") + assert state + assert state.state == "off" + assert state.attributes["options"] == ["off", "normal"] + set_node_attribute(matter_node, 1, 83, 2, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.laundrywasher_number_of_rinses") + assert state.state == "normal" diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 3215ec58116..251aab73e3b 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -193,6 +193,12 @@ async def test_battery_sensor_description( assert state assert state.state == "CR2032" + # case with a empty string to check if the attribute is indeed ignored + set_node_attribute(matter_node, 1, 47, 19, "") + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.smoke_sensor_battery_type") is None + @pytest.mark.parametrize("node_fixture", ["eve_thermo"]) async def test_eve_thermo_sensor( @@ -213,6 +219,18 @@ async def test_eve_thermo_sensor( assert state assert state.state == "0" + # LocalTemperature + state = hass.states.get("sensor.eve_thermo_temperature") + assert state + assert state.state == "21.0" + + set_node_attribute(matter_node, 1, 513, 0, 1800) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.eve_thermo_temperature") + assert state + assert state.state == "18.0" + @pytest.mark.parametrize("node_fixture", ["pressure_sensor"]) async def test_pressure_sensor( @@ -314,7 +332,7 @@ async def test_operational_state_sensor( matter_client: MagicMock, matter_node: MatterNode, ) -> None: - """Test dishwasher sensor.""" + """Test Operational State sensor, using a dishwasher fixture.""" # OperationalState Cluster / OperationalState attribute (1/96/4) state = hass.states.get("sensor.dishwasher_operational_state") assert state @@ -333,3 +351,51 @@ async def test_operational_state_sensor( state = hass.states.get("sensor.dishwasher_operational_state") assert state assert state.state == "extra_state" + + +@pytest.mark.parametrize("node_fixture", ["yandex_smart_socket"]) +async def test_draft_electrical_measurement_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test Draft Electrical Measurement cluster sensors, using Yandex Smart Socket fixture.""" + state = hass.states.get("sensor.yndx_00540_power") + assert state + assert state.state == "70.0" + + # AcPowerDivisor + set_node_attribute(matter_node, 1, 2820, 1541, 0) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.yndx_00540_power") + assert state + assert state.state == "unknown" + + # ActivePower + set_node_attribute(matter_node, 1, 2820, 1291, None) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.yndx_00540_power") + assert state + assert state.state == "unknown" + + +@pytest.mark.parametrize("node_fixture", ["silabs_laundrywasher"]) +async def test_list_sensor( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test Matter List sensor.""" + # OperationalState Cluster / CurrentPhase attribute (1/96/1) + state = hass.states.get("sensor.laundrywasher_current_phase") + assert state + assert state.state == "pre-soak" + + set_node_attribute(matter_node, 1, 96, 1, 1) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.laundrywasher_current_phase") + assert state + assert state.state == "rinse" diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index d7a6a700cde..e82848fcc3a 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -4,11 +4,14 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode +from matter_server.common.errors import MatterError +from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest from syrupy import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import ( @@ -110,3 +113,78 @@ async def test_power_switch(hass: HomeAssistant, matter_node: MatterNode) -> Non assert state assert state.state == "off" assert state.attributes["friendly_name"] == "Room AirConditioner Power" + + +@pytest.mark.parametrize("node_fixture", ["eve_thermo"]) +async def test_numeric_switch( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test numeric switch entity is discovered and working using an Eve Thermo fixture .""" + state = hass.states.get("switch.eve_thermo_child_lock") + assert state + assert state.state == "off" + # name should be derived from description attribute + assert state.attributes["friendly_name"] == "Eve Thermo Child lock" + # test attribute changes + set_node_attribute(matter_node, 1, 516, 1, 1) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("switch.eve_thermo_child_lock") + assert state.state == "on" + set_node_attribute(matter_node, 1, 516, 1, 0) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("switch.eve_thermo_child_lock") + assert state.state == "off" + # test switch service + await hass.services.async_call( + "switch", + "turn_on", + {"entity_id": "switch.eve_thermo_child_lock"}, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args_list[0] == call( + node_id=matter_node.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.ThermostatUserInterfaceConfiguration.Attributes.KeypadLockout, + ), + value=1, + ) + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.eve_thermo_child_lock"}, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 2 + assert matter_client.write_attribute.call_args_list[1] == call( + node_id=matter_node.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.ThermostatUserInterfaceConfiguration.Attributes.KeypadLockout, + ), + value=0, + ) + + +@pytest.mark.parametrize("node_fixture", ["on_off_plugin_unit"]) +async def test_matter_exception_on_command( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test if a MatterError gets converted to HomeAssistantError by using a switch fixture.""" + state = hass.states.get("switch.mock_onoffpluginunit") + assert state + matter_client.send_device_command.side_effect = MatterError("Boom") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", + "turn_on", + { + "entity_id": "switch.mock_onoffpluginunit", + }, + blocking=True, + ) diff --git a/tests/components/mcp/__init__.py b/tests/components/mcp/__init__.py new file mode 100644 index 00000000000..e8e8635ab36 --- /dev/null +++ b/tests/components/mcp/__init__.py @@ -0,0 +1 @@ +"""Tests for the Model Context Protocol integration.""" diff --git a/tests/components/mcp/conftest.py b/tests/components/mcp/conftest.py new file mode 100644 index 00000000000..d86603a12ed --- /dev/null +++ b/tests/components/mcp/conftest.py @@ -0,0 +1,45 @@ +"""Common fixtures for the Model Context Protocol tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.mcp.const import DOMAIN +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +TEST_API_NAME = "Memory Server" + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.mcp.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_mcp_client() -> Generator[AsyncMock]: + """Fixture to mock the MCP client.""" + with ( + patch("homeassistant.components.mcp.coordinator.sse_client"), + patch("homeassistant.components.mcp.coordinator.ClientSession") as mock_session, + ): + yield mock_session.return_value.__aenter__ + + +@pytest.fixture(name="config_entry") +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Fixture to load the integration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: "http://1.1.1.1/sse"}, + title=TEST_API_NAME, + ) + config_entry.add_to_hass(hass) + return config_entry diff --git a/tests/components/mcp/test_config_flow.py b/tests/components/mcp/test_config_flow.py new file mode 100644 index 00000000000..29733e653a6 --- /dev/null +++ b/tests/components/mcp/test_config_flow.py @@ -0,0 +1,234 @@ +"""Test the Model Context Protocol config flow.""" + +from typing import Any +from unittest.mock import AsyncMock, Mock + +import httpx +import pytest + +from homeassistant import config_entries +from homeassistant.components.mcp.const import DOMAIN +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import TEST_API_NAME + +from tests.common import MockConfigEntry + + +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_mcp_client: Mock +) -> None: + """Test the complete configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + response = Mock() + response.serverInfo.name = TEST_API_NAME + mock_mcp_client.return_value.initialize.return_value = response + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_API_NAME + assert result["data"] == { + CONF_URL: "http://1.1.1.1/sse", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (httpx.TimeoutException("Some timeout"), "timeout_connect"), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(500)), + "cannot_connect", + ), + (httpx.HTTPError("Some HTTP error"), "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_mcp_client_error( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mcp_client: Mock, + side_effect: Exception, + expected_error: str, +) -> None: + """Test we handle different client library errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_mcp_client.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} + + # Reset the error and make sure the config flow can resume successfully. + mock_mcp_client.side_effect = None + response = Mock() + response.serverInfo.name = TEST_API_NAME + mock_mcp_client.return_value.initialize.return_value = response + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_API_NAME + assert result["data"] == { + CONF_URL: "http://1.1.1.1/sse", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(401)), + "invalid_auth", + ), + ], +) +async def test_form_mcp_client_error_abort( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mcp_client: Mock, + side_effect: Exception, + expected_error: str, +) -> None: + """Test we handle different client library errors that end with an abort.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_mcp_client.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == expected_error + + +@pytest.mark.parametrize( + "user_input", + [ + ({CONF_URL: "not a url"}), + ({CONF_URL: "rtsp://1.1.1.1"}), + ], +) +async def test_input_form_validation_error( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mcp_client: Mock, + user_input: dict[str, Any], +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {CONF_URL: "invalid_url"} + + # Reset the error and make sure the config flow can resume successfully. + response = Mock() + response.serverInfo.name = TEST_API_NAME + mock_mcp_client.return_value.initialize.return_value = response + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_API_NAME + assert result["data"] == { + CONF_URL: "http://1.1.1.1/sse", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_unique_url( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_mcp_client: Mock +) -> None: + """Test that the same url cannot be configured twice.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: "http://1.1.1.1/sse"}, + title=TEST_API_NAME, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + response = Mock() + response.serverInfo.name = TEST_API_NAME + mock_mcp_client.return_value.initialize.return_value = response + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_server_missing_capbilities( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_mcp_client: Mock, +) -> None: + """Test we handle different client library errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + response = Mock() + response.serverInfo.name = TEST_API_NAME + response.capabilities.tools = None + mock_mcp_client.return_value.initialize.return_value = response + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: "http://1.1.1.1/sse", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "missing_capabilities" diff --git a/tests/components/mcp/test_init.py b/tests/components/mcp/test_init.py new file mode 100644 index 00000000000..460df2c5785 --- /dev/null +++ b/tests/components/mcp/test_init.py @@ -0,0 +1,225 @@ +"""Tests for the Model Context Protocol component.""" + +import re +from unittest.mock import Mock, patch + +import httpx +from mcp.types import CallToolResult, ListToolsResult, TextContent, Tool +import pytest +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import llm + +from .conftest import TEST_API_NAME + +from tests.common import MockConfigEntry + +SEARCH_MEMORY_TOOL = Tool( + name="search_memory", + description="Search memory for relevant context based on a query.", + inputSchema={ + "type": "object", + "required": ["query"], + "properties": { + "query": { + "type": "string", + "description": "A free text query to search context for.", + } + }, + }, +) +SAVE_MEMORY_TOOL = Tool( + name="save_memory", + description="Save a memory context.", + inputSchema={ + "type": "object", + "required": ["context"], + "properties": { + "context": { + "type": "object", + "description": "The context to save.", + "properties": { + "fact": { + "type": "string", + "description": "The key for the context.", + }, + }, + }, + }, + }, +) + + +def create_llm_context() -> llm.LLMContext: + """Create a test LLM context.""" + return llm.LLMContext( + platform="test_platform", + context=Context(), + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + + +async def test_init( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test the integration is initialized and can be unloaded cleanly.""" + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_mcp_server_failure( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test the integration fails to setup if the server fails initialization.""" + mock_mcp_client.side_effect = httpx.HTTPStatusError( + "", request=None, response=httpx.Response(500) + ) + + with patch("homeassistant.components.mcp.coordinator.TIMEOUT", 1): + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_list_tools_failure( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test the integration fails to load if the first data fetch returns an error.""" + mock_mcp_client.return_value.list_tools.side_effect = httpx.HTTPStatusError( + "", request=None, response=httpx.Response(500) + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_llm_get_api_tools( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test MCP tools are returned as LLM API tools.""" + mock_mcp_client.return_value.list_tools.return_value = ListToolsResult( + tools=[SEARCH_MEMORY_TOOL, SAVE_MEMORY_TOOL], + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + apis = llm.async_get_apis(hass) + api = next(iter([api for api in apis if api.name == TEST_API_NAME])) + assert api + + api_instance = await api.async_get_api_instance(create_llm_context()) + assert len(api_instance.tools) == 2 + tool = api_instance.tools[0] + assert tool.name == "search_memory" + assert tool.description == "Search memory for relevant context based on a query." + with pytest.raises( + vol.Invalid, match=re.escape("required key not provided @ data['query']") + ): + tool.parameters({}) + assert tool.parameters({"query": "frogs"}) == {"query": "frogs"} + + tool = api_instance.tools[1] + assert tool.name == "save_memory" + assert tool.description == "Save a memory context." + with pytest.raises( + vol.Invalid, match=re.escape("required key not provided @ data['context']") + ): + tool.parameters({}) + assert tool.parameters({"context": {"fact": "User was born in February"}}) == { + "context": {"fact": "User was born in February"} + } + + +async def test_call_tool( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test calling an MCP Tool through the LLM API.""" + mock_mcp_client.return_value.list_tools.return_value = ListToolsResult( + tools=[SEARCH_MEMORY_TOOL] + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + apis = llm.async_get_apis(hass) + api = next(iter([api for api in apis if api.name == TEST_API_NAME])) + assert api + + api_instance = await api.async_get_api_instance(create_llm_context()) + assert len(api_instance.tools) == 1 + tool = api_instance.tools[0] + assert tool.name == "search_memory" + + mock_mcp_client.return_value.call_tool.return_value = CallToolResult( + content=[TextContent(type="text", text="User was born in February")] + ) + result = await tool.async_call( + hass, + llm.ToolInput( + tool_name="search_memory", tool_args={"query": "User's birth month"} + ), + create_llm_context(), + ) + assert result == { + "content": [{"text": "User was born in February", "type": "text"}] + } + + +async def test_call_tool_fails( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test handling an MCP Tool call failure.""" + mock_mcp_client.return_value.list_tools.return_value = ListToolsResult( + tools=[SEARCH_MEMORY_TOOL] + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + apis = llm.async_get_apis(hass) + api = next(iter([api for api in apis if api.name == TEST_API_NAME])) + assert api + + api_instance = await api.async_get_api_instance(create_llm_context()) + assert len(api_instance.tools) == 1 + tool = api_instance.tools[0] + assert tool.name == "search_memory" + + mock_mcp_client.return_value.call_tool.side_effect = httpx.HTTPStatusError( + "Server error", request=None, response=httpx.Response(500) + ) + with pytest.raises( + HomeAssistantError, match="Error when calling tool: Server error" + ): + await tool.async_call( + hass, + llm.ToolInput( + tool_name="search_memory", tool_args={"query": "User's birth month"} + ), + create_llm_context(), + ) + + +async def test_convert_tool_schema_fails( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_mcp_client: Mock +) -> None: + """Test a failure converting an MCP tool schema to a Home Assistant schema.""" + mock_mcp_client.return_value.list_tools.return_value = ListToolsResult( + tools=[SEARCH_MEMORY_TOOL] + ) + + with patch( + "homeassistant.components.mcp.coordinator.convert_to_voluptuous", + side_effect=ValueError, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/mcp_server/__init__.py b/tests/components/mcp_server/__init__.py new file mode 100644 index 00000000000..6bdcc66de17 --- /dev/null +++ b/tests/components/mcp_server/__init__.py @@ -0,0 +1 @@ +"""Tests for the Model Context Protocol Server integration.""" diff --git a/tests/components/mcp_server/conftest.py b/tests/components/mcp_server/conftest.py new file mode 100644 index 00000000000..5ec67fb6ce3 --- /dev/null +++ b/tests/components/mcp_server/conftest.py @@ -0,0 +1,34 @@ +"""Common fixtures for the Model Context Protocol Server tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.mcp_server.const import DOMAIN, LLM_API +from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.mcp_server.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="config_entry") +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Fixture to load the integration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LLM_HASS_API: LLM_API, + }, + ) + config_entry.add_to_hass(hass) + return config_entry diff --git a/tests/components/mcp_server/test_config_flow.py b/tests/components/mcp_server/test_config_flow.py new file mode 100644 index 00000000000..3b9f5bee663 --- /dev/null +++ b/tests/components/mcp_server/test_config_flow.py @@ -0,0 +1,41 @@ +"""Test the Model Context Protocol Server config flow.""" + +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from homeassistant import config_entries +from homeassistant.components.mcp_server.const import DOMAIN +from homeassistant.const import CONF_LLM_HASS_API +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +@pytest.mark.parametrize( + "params", + [ + {}, + {CONF_LLM_HASS_API: "assist"}, + ], +) +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, params: dict[str, Any] +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + params, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Assist" + assert len(mock_setup_entry.mock_calls) == 1 + assert result["data"] == {CONF_LLM_HASS_API: "assist"} diff --git a/tests/components/mcp_server/test_http.py b/tests/components/mcp_server/test_http.py new file mode 100644 index 00000000000..905bfaa11d7 --- /dev/null +++ b/tests/components/mcp_server/test_http.py @@ -0,0 +1,426 @@ +"""Test the Model Context Protocol Server init module.""" + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from http import HTTPStatus +import json +import logging + +import aiohttp +import mcp +import mcp.client.session +import mcp.client.sse +from mcp.shared.exceptions import McpError +import pytest + +from homeassistant.components.conversation import DOMAIN as CONVERSATION_DOMAIN +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.mcp_server.http import MESSAGES_API, SSE_API +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_LLM_HASS_API, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, +) +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, setup_test_component_platform +from tests.components.light.common import MockLight +from tests.typing import ClientSessionGenerator + +_LOGGER = logging.getLogger(__name__) + +TEST_ENTITY = "light.kitchen" +INITIALIZE_MESSAGE = { + "jsonrpc": "2.0", + "id": "request-id-1", + "method": "initialize", + "params": { + "protocolVersion": "1.0", + "capabilities": {}, + "clientInfo": { + "name": "test", + "version": "1", + }, + }, +} +EVENT_PREFIX = "event: " +DATA_PREFIX = "data: " +EXPECTED_PROMPT_SUFFIX = """ +- names: Kitchen Light + domain: light + areas: Kitchen +""" + + +@pytest.fixture +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the config entry.""" + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + +@pytest.fixture(autouse=True) +async def mock_entities( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, + setup_integration: None, +) -> None: + """Fixture to expose entities to the conversation agent.""" + entity = MockLight("Kitchen Light", STATE_OFF) + entity.entity_id = TEST_ENTITY + entity.unique_id = "test-light-unique-id" + setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: [{"platform": "test"}]}, + ) + await hass.async_block_till_done() + kitchen = area_registry.async_get_or_create("Kitchen") + entity_registry.async_update_entity(TEST_ENTITY, area_id=kitchen.id) + + async_expose_entity(hass, CONVERSATION_DOMAIN, TEST_ENTITY, True) + + +async def sse_response_reader( + response: aiohttp.ClientResponse, +) -> AsyncGenerator[tuple[str, str]]: + """Read SSE responses from the server and emit event messages. + + SSE responses are formatted as: + event: event-name + data: event-data + and this function emits each event-name and event-data as a tuple. + """ + it = aiter(response.content) + while True: + line = (await anext(it)).decode() + if not line.startswith(EVENT_PREFIX): + raise ValueError("Expected event") + event = line[len(EVENT_PREFIX) :].strip() + line = (await anext(it)).decode() + if not line.startswith(DATA_PREFIX): + raise ValueError("Expected data") + data = line[len(DATA_PREFIX) :].strip() + line = (await anext(it)).decode() + assert line == "\r\n" + yield event, data + + +async def test_http_sse( + hass: HomeAssistant, + setup_integration: None, + hass_client: ClientSessionGenerator, +) -> None: + """Test SSE endpoint can be used to receive MCP messages.""" + + client = await hass_client() + + # Start an SSE session + response = await client.get(SSE_API) + assert response.status == HTTPStatus.OK + + # Decode a single SSE response that sends the messages endpoint + reader = sse_response_reader(response) + event, endpoint_url = await anext(reader) + assert event == "endpoint" + + # Send an initialize message on the messages endpoint + response = await client.post(endpoint_url, json=INITIALIZE_MESSAGE) + assert response.status == HTTPStatus.OK + + # Decode the initialize response event message from the SSE stream + event, data = await anext(reader) + assert event == "message" + message = json.loads(data) + assert message.get("jsonrpc") == "2.0" + assert message.get("id") == "request-id-1" + assert "serverInfo" in message.get("result", {}) + assert "protocolVersion" in message.get("result", {}) + + +async def test_http_messages_missing_session_id( + hass: HomeAssistant, + setup_integration: None, + hass_client: ClientSessionGenerator, +) -> None: + """Test the tools list endpoint.""" + + client = await hass_client() + response = await client.post(MESSAGES_API.format(session_id="invalid-session-id")) + assert response.status == HTTPStatus.NOT_FOUND + response_data = await response.text() + assert response_data == "Could not find session ID 'invalid-session-id'" + + +async def test_http_messages_invalid_message_format( + hass: HomeAssistant, + setup_integration: None, + hass_client: ClientSessionGenerator, +) -> None: + """Test the tools list endpoint.""" + + client = await hass_client() + response = await client.get(SSE_API) + assert response.status == HTTPStatus.OK + reader = sse_response_reader(response) + event, endpoint_url = await anext(reader) + assert event == "endpoint" + + response = await client.post(endpoint_url, json={"invalid": "message"}) + assert response.status == HTTPStatus.BAD_REQUEST + response_data = await response.text() + assert response_data == "Could not parse message" + + +async def test_http_sse_multiple_config_entries( + hass: HomeAssistant, + setup_integration: None, + hass_client: ClientSessionGenerator, +) -> None: + """Test the SSE endpoint will fail with multiple config entries. + + This cannot happen in practice as the integration only supports a single + config entry, but this is added for test coverage. + """ + + config_entry = MockConfigEntry( + domain="mcp_server", data={CONF_LLM_HASS_API: "llm-api-id"} + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + + client = await hass_client() + + # Attempt to start an SSE session will fail + response = await client.get(SSE_API) + assert response.status == HTTPStatus.NOT_FOUND + response_data = await response.text() + assert "Found multiple Model Context Protocol" in response_data + + +async def test_http_sse_no_config_entry( + hass: HomeAssistant, + setup_integration: None, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test the SSE endpoint fails with a missing config entry.""" + + await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED + + client = await hass_client() + + # Start an SSE session + response = await client.get(SSE_API) + assert response.status == HTTPStatus.NOT_FOUND + response_data = await response.text() + assert "Model Context Protocol server is not configured" in response_data + + +async def test_http_messages_no_config_entry( + hass: HomeAssistant, + setup_integration: None, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test the message endpoint will fail if the config entry is unloaded.""" + + client = await hass_client() + + # Start an SSE session + response = await client.get(SSE_API) + assert response.status == HTTPStatus.OK + reader = sse_response_reader(response) + event, endpoint_url = await anext(reader) + assert event == "endpoint" + + # Invalidate the session by unloading the config entry + await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED + + # Reload the config entry and ensure the session is not found + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + response = await client.post(endpoint_url, json=INITIALIZE_MESSAGE) + assert response.status == HTTPStatus.NOT_FOUND + response_data = await response.text() + assert "Could not find session ID" in response_data + + +async def test_http_requires_authentication( + hass: HomeAssistant, + setup_integration: None, + hass_client_no_auth: ClientSessionGenerator, +) -> None: + """Test the SSE endpoint requires authentication.""" + + client = await hass_client_no_auth() + + response = await client.get(SSE_API) + assert response.status == HTTPStatus.UNAUTHORIZED + + response = await client.post(MESSAGES_API.format(session_id="session-id")) + assert response.status == HTTPStatus.UNAUTHORIZED + + +@pytest.fixture +async def mcp_sse_url(hass_client: ClientSessionGenerator) -> str: + """Fixture to get the MCP integration SSE URL.""" + client = await hass_client() + return str(client.make_url(SSE_API)) + + +@asynccontextmanager +async def mcp_session( + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> AsyncGenerator[mcp.client.session.ClientSession]: + """Create an MCP session.""" + + headers = {"Authorization": f"Bearer {hass_supervisor_access_token}"} + + async with ( + mcp.client.sse.sse_client(mcp_sse_url, headers=headers) as streams, + mcp.client.session.ClientSession(*streams) as session, + ): + await session.initialize() + yield session + + +async def test_mcp_tools_list( + hass: HomeAssistant, + setup_integration: None, + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> None: + """Test the tools list endpoint.""" + + async with mcp_session(mcp_sse_url, hass_supervisor_access_token) as session: + result = await session.list_tools() + + # Pick a single arbitrary tool and test that description and parameters + # are converted correctly. + tool = next(iter(tool for tool in result.tools if tool.name == "HassTurnOn")) + assert tool.name == "HassTurnOn" + assert tool.description == "Turns on/opens a device or entity" + assert tool.inputSchema + assert tool.inputSchema.get("type") == "object" + properties = tool.inputSchema.get("properties") + assert properties.get("name") == {"type": "string"} + + +async def test_mcp_tool_call( + hass: HomeAssistant, + setup_integration: None, + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> None: + """Test the tool call endpoint.""" + + state = hass.states.get("light.kitchen") + assert state + assert state.state == STATE_OFF + + async with mcp_session(mcp_sse_url, hass_supervisor_access_token) as session: + result = await session.call_tool( + name="HassTurnOn", + arguments={"name": "kitchen light"}, + ) + + assert not result.isError + assert len(result.content) == 1 + assert result.content[0].type == "text" + # The content is the raw tool call payload + content = json.loads(result.content[0].text) + assert content.get("data", {}).get("success") + assert not content.get("data", {}).get("failed") + + # Verify tool call invocation + state = hass.states.get("light.kitchen") + assert state + assert state.state == STATE_ON + + +async def test_mcp_tool_call_failed( + hass: HomeAssistant, + setup_integration: None, + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> None: + """Test the tool call endpoint with a failure.""" + + async with mcp_session(mcp_sse_url, hass_supervisor_access_token) as session: + result = await session.call_tool( + name="HassTurnOn", + arguments={"name": "backyard"}, + ) + + assert result.isError + assert len(result.content) == 1 + assert result.content[0].type == "text" + assert "Error calling tool" in result.content[0].text + + +async def test_prompt_list( + hass: HomeAssistant, + setup_integration: None, + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> None: + """Test the list prompt endpoint.""" + + async with mcp_session(mcp_sse_url, hass_supervisor_access_token) as session: + result = await session.list_prompts() + + assert len(result.prompts) == 1 + prompt = result.prompts[0] + assert prompt.name == "Stateless Assist" + assert ( + prompt.description + == "Default prompt for the Home Assistant LLM API Stateless Assist" + ) + + +async def test_prompt_get( + hass: HomeAssistant, + setup_integration: None, + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> None: + """Test the get prompt endpoint.""" + + async with mcp_session(mcp_sse_url, hass_supervisor_access_token) as session: + result = await session.get_prompt(name="Stateless Assist") + + assert ( + result.description + == "Default prompt for the Home Assistant LLM API Stateless Assist" + ) + assert len(result.messages) == 1 + assert result.messages[0].role == "assistant" + assert result.messages[0].content.type == "text" + assert "When controlling Home Assistant" in result.messages[0].content.text + assert result.messages[0].content.text.endswith(EXPECTED_PROMPT_SUFFIX) + + +async def test_get_unknwon_prompt( + hass: HomeAssistant, + setup_integration: None, + mcp_sse_url: str, + hass_supervisor_access_token: str, +) -> None: + """Test the get prompt endpoint.""" + + async with mcp_session(mcp_sse_url, hass_supervisor_access_token) as session: + with pytest.raises(McpError): + await session.get_prompt(name="Unknown") diff --git a/tests/components/mcp_server/test_init.py b/tests/components/mcp_server/test_init.py new file mode 100644 index 00000000000..af6a8a55e59 --- /dev/null +++ b/tests/components/mcp_server/test_init.py @@ -0,0 +1,15 @@ +"""Test the Model Context Protocol Server init module.""" + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_init(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Test the integration is initialized and can be unloaded cleanly.""" + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index e5a0a697157..7587a7a55b7 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -170,6 +170,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -222,6 +223,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -274,6 +276,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -326,6 +329,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index 98ca52dd15e..aada173ffc3 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/mealie/snapshots/test_sensor.ambr b/tests/components/mealie/snapshots/test_sensor.ambr index e645cf4c45f..19219c01c1c 100644 --- a/tests/components/mealie/snapshots/test_sensor.ambr +++ b/tests/components/mealie/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -108,6 +110,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -158,6 +161,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -208,6 +212,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 4f9ee6a5c09..56626c7b5c4 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -758,6 +758,32 @@ }), }) # --- +# name: test_service_set_mealplan[payload2-kwargs2] + dict({ + 'mealplan': dict({ + 'description': None, + 'entry_type': , + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, + 'mealplan_date': datetime.date(2024, 1, 22), + 'mealplan_id': 230, + 'recipe': dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'household_id': None, + 'image': 'AiIo', + 'name': 'Zoete aardappel curry traybake', + 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', + 'recipe_id': 'c5f00a93-71a2-4e48-900f-d9ad0bb9de93', + 'recipe_yield': '2 servings', + 'slug': 'zoete-aardappel-curry-traybake', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + 'title': None, + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + }) +# --- # name: test_service_set_random_mealplan dict({ 'mealplan': dict({ diff --git a/tests/components/mealie/snapshots/test_todo.ambr b/tests/components/mealie/snapshots/test_todo.ambr index 4c58a839f57..88c677de581 100644 --- a/tests/components/mealie/snapshots/test_todo.ambr +++ b/tests/components/mealie/snapshots/test_todo.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/mealie/test_calendar.py b/tests/components/mealie/test_calendar.py index d11fe5d2354..cca4fcca673 100644 --- a/tests/components/mealie/test_calendar.py +++ b/tests/components/mealie/test_calendar.py @@ -4,9 +4,10 @@ from datetime import date from http import HTTPStatus from unittest.mock import AsyncMock, patch +from aiomealie import MealplanResponse from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -40,13 +41,28 @@ async def test_entities( mock_mealie_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: - """Test the API returns the calendar.""" + """Test the calendar entities.""" with patch("homeassistant.components.mealie.PLATFORMS", [Platform.CALENDAR]): await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +async def test_no_meal_planned( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the calendar handles no meal planned.""" + mock_mealie_client.get_mealplans.return_value = MealplanResponse([]) + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("calendar.mealie_dinner").state == STATE_OFF + + async def test_api_events( hass: HomeAssistant, snapshot: SnapshotAssertion, diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 1c8c6f19de7..63668379490 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -250,6 +250,12 @@ async def test_service_set_random_mealplan( }, {"recipe_id": None, "note_title": "Note Title", "note_text": "Note Text"}, ), + ( + { + ATTR_NOTE_TITLE: "Note Title", + }, + {"recipe_id": None, "note_title": "Note Title", "note_text": None}, + ), ], ) async def test_service_set_mealplan( diff --git a/tests/components/mealie/test_todo.py b/tests/components/mealie/test_todo.py index 920cfc47397..e7942887099 100644 --- a/tests/components/mealie/test_todo.py +++ b/tests/components/mealie/test_todo.py @@ -1,9 +1,9 @@ """Tests for the Mealie todo.""" from datetime import timedelta -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, call, patch -from aiomealie import MealieError, ShoppingListsResponse +from aiomealie import MealieError, MutateShoppingItem, ShoppingListsResponse from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -18,7 +18,7 @@ from homeassistant.components.todo import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -29,6 +29,7 @@ from tests.common import ( load_fixture, snapshot_platform, ) +from tests.typing import WebSocketGenerator async def test_entities( @@ -45,23 +46,38 @@ async def test_entities( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_add_todo_list_item( +@pytest.mark.parametrize( + ("service", "data", "method"), + [ + (TodoServices.ADD_ITEM, {ATTR_ITEM: "Soda"}, "add_shopping_item"), + ( + TodoServices.UPDATE_ITEM, + {ATTR_ITEM: "aubergine", ATTR_RENAME: "Eggplant", ATTR_STATUS: "completed"}, + "update_shopping_item", + ), + (TodoServices.REMOVE_ITEM, {ATTR_ITEM: "aubergine"}, "delete_shopping_item"), + ], +) +async def test_todo_actions( hass: HomeAssistant, mock_mealie_client: AsyncMock, mock_config_entry: MockConfigEntry, + service: str, + data: dict[str, str], + method: str, ) -> None: - """Test for adding a To-do Item.""" + """Test todo actions.""" await setup_integration(hass, mock_config_entry) await hass.services.async_call( TODO_DOMAIN, - TodoServices.ADD_ITEM, - {ATTR_ITEM: "Soda"}, + service, + data, target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, blocking=True, ) - mock_mealie_client.add_shopping_item.assert_called_once() + getattr(mock_mealie_client, method).assert_called_once() async def test_add_todo_list_item_error( @@ -74,7 +90,9 @@ async def test_add_todo_list_item_error( mock_mealie_client.add_shopping_item.side_effect = MealieError - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match="An error occurred adding an item to Supermarket" + ): await hass.services.async_call( TODO_DOMAIN, TodoServices.ADD_ITEM, @@ -84,25 +102,6 @@ async def test_add_todo_list_item_error( ) -async def test_update_todo_list_item( - hass: HomeAssistant, - mock_mealie_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test for updating a To-do Item.""" - await setup_integration(hass, mock_config_entry) - - await hass.services.async_call( - TODO_DOMAIN, - TodoServices.UPDATE_ITEM, - {ATTR_ITEM: "aubergine", ATTR_RENAME: "Eggplant", ATTR_STATUS: "completed"}, - target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, - blocking=True, - ) - - mock_mealie_client.update_shopping_item.assert_called_once() - - async def test_update_todo_list_item_error( hass: HomeAssistant, mock_mealie_client: AsyncMock, @@ -113,7 +112,9 @@ async def test_update_todo_list_item_error( mock_mealie_client.update_shopping_item.side_effect = MealieError - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match="An error occurred updating an item in Supermarket" + ): await hass.services.async_call( TODO_DOMAIN, TodoServices.UPDATE_ITEM, @@ -123,23 +124,24 @@ async def test_update_todo_list_item_error( ) -async def test_delete_todo_list_item( +async def test_update_non_existent_item( hass: HomeAssistant, mock_mealie_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: - """Test for deleting a To-do Item.""" + """Test for updating a non-existent To-do Item.""" await setup_integration(hass, mock_config_entry) - await hass.services.async_call( - TODO_DOMAIN, - TodoServices.REMOVE_ITEM, - {ATTR_ITEM: "aubergine"}, - target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, - blocking=True, - ) - - mock_mealie_client.delete_shopping_item.assert_called_once() + with pytest.raises( + ServiceValidationError, match="Unable to find to-do list item: eggplant" + ): + await hass.services.async_call( + TODO_DOMAIN, + TodoServices.UPDATE_ITEM, + {ATTR_ITEM: "eggplant", ATTR_RENAME: "Aubergine", ATTR_STATUS: "completed"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + ) async def test_delete_todo_list_item_error( @@ -153,7 +155,9 @@ async def test_delete_todo_list_item_error( mock_mealie_client.delete_shopping_item = AsyncMock() mock_mealie_client.delete_shopping_item.side_effect = MealieError - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match="An error occurred deleting an item in Supermarket" + ): await hass.services.async_call( TODO_DOMAIN, TodoServices.REMOVE_ITEM, @@ -163,6 +167,172 @@ async def test_delete_todo_list_item_error( ) +async def test_moving_todo_item( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test for moving a To-do Item to place.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "todo/item/move", + "entity_id": "todo.mealie_supermarket", + "uid": "f45430f7-3edf-45a9-a50f-73bb375090be", + "previous_uid": "84d8fd74-8eb0-402e-84b6-71f251bfb7cc", + } + ) + resp = await client.receive_json() + assert resp.get("id") == 1 + assert resp.get("success") + assert resp.get("result") is None + + assert mock_mealie_client.update_shopping_item.call_count == 3 + calls = mock_mealie_client.update_shopping_item.mock_calls + + assert calls[0] == call( + "84d8fd74-8eb0-402e-84b6-71f251bfb7cc", + MutateShoppingItem( + item_id="84d8fd74-8eb0-402e-84b6-71f251bfb7cc", + list_id="9ce096fe-ded2-4077-877d-78ba450ab13e", + note="", + display=None, + checked=False, + position=0, + is_food=True, + disable_amount=None, + quantity=1.0, + label_id=None, + food_id="09322430-d24c-4b1a-abb6-22b6ed3a88f5", + unit_id="7bf539d4-fc78-48bc-b48e-c35ccccec34a", + ), + ) + + assert calls[1] == call( + "f45430f7-3edf-45a9-a50f-73bb375090be", + MutateShoppingItem( + item_id="f45430f7-3edf-45a9-a50f-73bb375090be", + list_id="9ce096fe-ded2-4077-877d-78ba450ab13e", + note="Apples", + display=None, + checked=False, + position=1, + is_food=False, + disable_amount=None, + quantity=2.0, + label_id=None, + food_id=None, + unit_id=None, + ), + ) + + assert calls[2] == call( + "69913b9a-7c75-4935-abec-297cf7483f88", + MutateShoppingItem( + item_id="69913b9a-7c75-4935-abec-297cf7483f88", + list_id="9ce096fe-ded2-4077-877d-78ba450ab13e", + note="", + display=None, + checked=False, + position=2, + is_food=True, + disable_amount=None, + quantity=0.0, + label_id=None, + food_id="96801494-4e26-4148-849a-8155deb76327", + unit_id=None, + ), + ) + + +async def test_not_moving_todo_item( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test for moving a To-do Item to the same place.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "todo/item/move", + "entity_id": "todo.mealie_supermarket", + "uid": "f45430f7-3edf-45a9-a50f-73bb375090be", + "previous_uid": "f45430f7-3edf-45a9-a50f-73bb375090be", + } + ) + resp = await client.receive_json() + assert resp.get("id") == 1 + assert resp.get("success") + assert resp.get("result") is None + + assert mock_mealie_client.update_shopping_item.call_count == 0 + + +async def test_moving_todo_item_invalid_uid( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test for moving a To-do Item to place with invalid UID.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "todo/item/move", + "entity_id": "todo.mealie_supermarket", + "uid": "cheese", + } + ) + resp = await client.receive_json() + assert resp.get("id") == 1 + assert resp.get("success") is False + assert resp.get("result") is None + assert resp["error"]["code"] == "failed" + assert resp["error"]["message"] == "Item cheese not found" + + assert mock_mealie_client.update_shopping_item.call_count == 0 + + +async def test_moving_todo_item_invalid_previous_uid( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test for moving a To-do Item to place with invalid previous UID.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "todo/item/move", + "entity_id": "todo.mealie_supermarket", + "uid": "f45430f7-3edf-45a9-a50f-73bb375090be", + "previous_uid": "cheese", + } + ) + resp = await client.receive_json() + assert resp.get("id") == 1 + assert resp.get("success") is False + assert resp.get("result") is None + assert resp["error"]["code"] == "failed" + assert resp["error"]["message"] == "Item cheese not found" + + assert mock_mealie_client.update_shopping_item.call_count == 0 + + async def test_runtime_management( hass: HomeAssistant, mock_mealie_client: AsyncMock, diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 750d2861f21..680603c097d 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.media_player as mp +from homeassistant.components import media_player as mp from homeassistant.const import ( STATE_IDLE, STATE_OFF, diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index 4bb27b73f24..ae3a84e66a0 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 7c64f846df1..38486fe5911 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -10,18 +10,25 @@ import voluptuous as vol from homeassistant.components import media_player from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, BrowseMedia, MediaClass, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.const import SERVICE_BROWSE_MEDIA from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import help_test_all, import_and_test_deprecated_constant_enum +from tests.common import ( + MockEntityPlatform, + help_test_all, + import_and_test_deprecated_constant_enum, +) from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -116,20 +123,28 @@ def test_deprecated_constants_const( "grouping", ], ) -def test_support_properties(property_suffix: str) -> None: +def test_support_properties(hass: HomeAssistant, property_suffix: str) -> None: """Test support_*** properties explicitly.""" all_features = media_player.MediaPlayerEntityFeature(653887) feature = media_player.MediaPlayerEntityFeature[property_suffix.upper()] entity1 = MediaPlayerEntity() + entity1.hass = hass + entity1.platform = MockEntityPlatform(hass) entity1._attr_supported_features = media_player.MediaPlayerEntityFeature(0) entity2 = MediaPlayerEntity() + entity2.hass = hass + entity2.platform = MockEntityPlatform(hass) entity2._attr_supported_features = all_features entity3 = MediaPlayerEntity() + entity3.hass = hass + entity3.platform = MockEntityPlatform(hass) entity3._attr_supported_features = feature entity4 = MediaPlayerEntity() - entity4._attr_supported_features = all_features & ~feature + entity4.hass = hass + entity4.platform = MockEntityPlatform(hass) + entity4._attr_supported_features = all_features - feature assert getattr(entity1, f"support_{property_suffix}") is False assert getattr(entity2, f"support_{property_suffix}") is True @@ -152,8 +167,7 @@ async def test_get_image_http( client = await hass_client_no_auth() with patch( - "homeassistant.components.media_player.MediaPlayerEntity." - "async_get_media_image", + "homeassistant.components.media_player.MediaPlayerEntity.async_get_media_image", return_value=(b"image", "image/jpeg"), ): resp = await client.get(state.attributes["entity_picture"]) @@ -328,6 +342,75 @@ async def test_media_browse( assert msg["result"] == {"bla": "yo"} +async def test_media_browse_service(hass: HomeAssistant) -> None: + """Test browsing media using service call.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.demo.media_player.DemoBrowsePlayer.async_browse_media", + return_value=BrowseMedia( + media_class=MediaClass.DIRECTORY, + media_content_id="mock-id", + media_content_type="mock-type", + title="Mock Title", + can_play=False, + can_expand=True, + children=[ + BrowseMedia( + media_class=MediaClass.ALBUM, + media_content_id="album1 content id", + media_content_type="album", + title="Album 1", + can_play=True, + can_expand=True, + ), + BrowseMedia( + media_class=MediaClass.ALBUM, + media_content_id="album2 content id", + media_content_type="album", + title="Album 2", + can_play=True, + can_expand=True, + ), + ], + ), + ) as mock_browse_media: + result = await hass.services.async_call( + "media_player", + SERVICE_BROWSE_MEDIA, + { + ATTR_ENTITY_ID: "media_player.browse", + ATTR_MEDIA_CONTENT_TYPE: "album", + ATTR_MEDIA_CONTENT_ID: "title=Album*", + }, + blocking=True, + return_response=True, + ) + + mock_browse_media.assert_called_with( + media_content_type="album", media_content_id="title=Album*" + ) + browse_res: BrowseMedia = result["media_player.browse"] + assert browse_res.title == "Mock Title" + assert browse_res.media_class == "directory" + assert browse_res.media_content_type == "mock-type" + assert browse_res.media_content_id == "mock-id" + assert browse_res.can_play is False + assert browse_res.can_expand is True + assert len(browse_res.children) == 2 + assert browse_res.children[0].title == "Album 1" + assert browse_res.children[0].media_class == "album" + assert browse_res.children[0].media_content_id == "album1 content id" + assert browse_res.children[0].media_content_type == "album" + assert browse_res.children[1].title == "Album 2" + assert browse_res.children[1].media_class == "album" + assert browse_res.children[1].media_content_id == "album2 content id" + assert browse_res.children[1].media_content_type == "album" + + async def test_group_members_available_when_off(hass: HomeAssistant) -> None: """Test that group_members are still available when media_player is off.""" await async_setup_component( @@ -447,3 +530,27 @@ async def test_get_async_get_browse_image_quoting( url = player.get_browse_image_url("album", media_content_id) await client.get(url) mock_browse_image.assert_called_with("album", media_content_id, None) + + +def test_deprecated_supported_features_ints( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test deprecated supported features ints.""" + + class MockMediaPlayerEntity(MediaPlayerEntity): + @property + def supported_features(self) -> int: + """Return supported features.""" + return 1 + + entity = MockMediaPlayerEntity() + entity.hass = hass + entity.platform = MockEntityPlatform(hass) + assert entity.supported_features_compat is MediaPlayerEntityFeature(1) + assert "MockMediaPlayerEntity" in caplog.text + assert "is using deprecated supported features values" in caplog.text + assert "Instead it should use" in caplog.text + assert "MediaPlayerEntityFeature.PAUSE" in caplog.text + caplog.clear() + assert entity.supported_features_compat is MediaPlayerEntityFeature(1) + assert "is using deprecated supported features values" not in caplog.text diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index ceb14faf8fb..b305d629a91 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -10,7 +10,7 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_integration diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index a2ba23d9e61..23902a4b780 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( mock_config_entry, diff --git a/tests/components/melnor/test_time.py b/tests/components/melnor/test_time.py index 50b51d31ff8..f8a3adcf3d0 100644 --- a/tests/components/melnor/test_time.py +++ b/tests/components/melnor/test_time.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import time, timedelta from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( mock_config_entry, diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py index 123fc00e42a..eb28ec0a838 100644 --- a/tests/components/meteo_france/conftest.py +++ b/tests/components/meteo_france/conftest.py @@ -2,13 +2,48 @@ from unittest.mock import patch +from meteofrance_api.model import CurrentPhenomenons, Forecast, Rain import pytest +from homeassistant.components.meteo_france.const import CONF_CITY, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_json_object_fixture + @pytest.fixture(autouse=True) def patch_requests(): """Stub out services that makes requests.""" - patch_client = patch("homeassistant.components.meteo_france.MeteoFranceClient") + with patch("homeassistant.components.meteo_france.MeteoFranceClient") as mock_data: + mock_data = mock_data.return_value + mock_data.get_forecast.return_value = Forecast( + load_json_object_fixture("raw_forecast.json", DOMAIN) + ) + mock_data.get_rain.return_value = Rain( + load_json_object_fixture("raw_rain.json", DOMAIN) + ) + mock_data.get_warning_current_phenomenoms.return_value = CurrentPhenomenons( + load_json_object_fixture("raw_warning_current_phenomenoms.json", DOMAIN) + ) + yield mock_data - with patch_client: - yield + +@pytest.fixture(name="config_entry") +def get_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create and register mock config entry.""" + entry_data = { + CONF_CITY: "La Clusaz", + CONF_LATITUDE: 45.90417, + CONF_LONGITUDE: 6.42306, + } + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + unique_id=f"{entry_data[CONF_LATITUDE], entry_data[CONF_LONGITUDE]}", + title=entry_data[CONF_CITY], + data=entry_data, + ) + config_entry.add_to_hass(hass) + return config_entry diff --git a/tests/components/meteo_france/fixtures/raw_forecast.json b/tests/components/meteo_france/fixtures/raw_forecast.json new file mode 100644 index 00000000000..3c0552136d2 --- /dev/null +++ b/tests/components/meteo_france/fixtures/raw_forecast.json @@ -0,0 +1,53 @@ +{ + "updated_on": 1737995400, + "position": { + "country": "FR - France", + "dept": "74", + "insee": "74080", + "lat": 45.90417, + "lon": 6.42306, + "name": "La Clusaz", + "rain_product_available": 1, + "timezone": "Europe/Paris" + }, + "daily_forecast": [ + { + "T": { "max": 10.4, "min": 6.9, "sea": null }, + "dt": 1737936000, + "humidity": { "max": 90, "min": 65 }, + "precipitation": { "24h": 1.3 }, + "sun": { "rise": 1737963392, "set": 1737996163 }, + "uv": 1, + "weather12H": { "desc": "Eclaircies", "icon": "p2j" } + } + ], + "forecast": [ + { + "T": { "value": 9.1, "windchill": 5.4 }, + "clouds": 70, + "dt": 1737990000, + "humidity": 75, + "iso0": 1250, + "rain": { "1h": 0 }, + "rain snow limit": "Non pertinent", + "sea_level": 988.7, + "snow": { "1h": 0 }, + "uv": 1, + "weather": { "desc": "Eclaircies", "icon": "p2j" }, + "wind": { + "direction": 200, + "gust": 18, + "icon": "SSO", + "speed": 8 + } + } + ], + "probability_forecast": [ + { + "dt": 1737990000, + "freezing": 0, + "rain": { "3h": null, "6h": null }, + "snow": { "3h": null, "6h": null } + } + ] +} diff --git a/tests/components/meteo_france/fixtures/raw_rain.json b/tests/components/meteo_france/fixtures/raw_rain.json new file mode 100644 index 00000000000..a9f17b8a98e --- /dev/null +++ b/tests/components/meteo_france/fixtures/raw_rain.json @@ -0,0 +1,24 @@ +{ + "position": { + "lat": 48.807166, + "lon": 2.239895, + "alti": 76, + "name": "Meudon", + "country": "FR - France", + "dept": "92", + "timezone": "Europe/Paris" + }, + "updated_on": 1589995200, + "quality": 0, + "forecast": [ + { "dt": 1589996100, "rain": 1, "desc": "Temps sec" }, + { "dt": 1589996400, "rain": 1, "desc": "Temps sec" }, + { "dt": 1589996700, "rain": 1, "desc": "Temps sec" }, + { "dt": 1589997000, "rain": 2, "desc": "Pluie faible" }, + { "dt": 1589997300, "rain": 3, "desc": "Pluie modérée" }, + { "dt": 1589997600, "rain": 2, "desc": "Pluie faible" }, + { "dt": 1589998200, "rain": 1, "desc": "Temps sec" }, + { "dt": 1589998800, "rain": 1, "desc": "Temps sec" }, + { "dt": 1589999400, "rain": 1, "desc": "Temps sec" } + ] +} diff --git a/tests/components/meteo_france/fixtures/raw_warning_current_phenomenoms.json b/tests/components/meteo_france/fixtures/raw_warning_current_phenomenoms.json new file mode 100644 index 00000000000..8d84e512fb6 --- /dev/null +++ b/tests/components/meteo_france/fixtures/raw_warning_current_phenomenoms.json @@ -0,0 +1,13 @@ +{ + "update_time": 1591279200, + "end_validity_time": 1591365600, + "domain_id": "32", + "phenomenons_max_colors": [ + { "phenomenon_id": "6", "phenomenon_max_color_id": 1 }, + { "phenomenon_id": "4", "phenomenon_max_color_id": 1 }, + { "phenomenon_id": "5", "phenomenon_max_color_id": 3 }, + { "phenomenon_id": "2", "phenomenon_max_color_id": 1 }, + { "phenomenon_id": "1", "phenomenon_max_color_id": 1 }, + { "phenomenon_id": "3", "phenomenon_max_color_id": 2 } + ] +} diff --git a/tests/components/meteo_france/snapshots/test_sensor.ambr b/tests/components/meteo_france/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..35b6a9d19f7 --- /dev/null +++ b/tests/components/meteo_france/snapshots/test_sensor.ambr @@ -0,0 +1,779 @@ +# serializer version: 1 +# name: test_sensor[sensor.32_weather_alert-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.32_weather_alert', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-cloudy-alert', + 'original_name': '32 Weather alert', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '32 Weather alert', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.32_weather_alert-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'Canicule': 'Vert', + 'Inondation': 'Vert', + 'Neige-verglas': 'Orange', + 'Orages': 'Jaune', + 'Pluie-inondation': 'Vert', + 'Vent violent': 'Vert', + 'attribution': 'Data provided by Météo-France', + 'friendly_name': '32 Weather alert', + 'icon': 'mdi:weather-cloudy-alert', + }), + 'context': , + 'entity_id': 'sensor.32_weather_alert', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Orange', + }) +# --- +# name: test_sensor[sensor.la_clusaz_cloud_cover-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_cloud_cover', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-partly-cloudy', + 'original_name': 'La Clusaz Cloud cover', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_cloud', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.la_clusaz_cloud_cover-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz Cloud cover', + 'icon': 'mdi:weather-partly-cloudy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_cloud_cover', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '70', + }) +# --- +# name: test_sensor[sensor.la_clusaz_daily_original_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_daily_original_condition', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'La Clusaz Daily original condition', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_daily_original_condition', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.la_clusaz_daily_original_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz Daily original condition', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_daily_original_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Eclaircies', + }) +# --- +# name: test_sensor[sensor.la_clusaz_daily_precipitation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_daily_precipitation', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'La Clusaz Daily precipitation', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_precipitation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.la_clusaz_daily_precipitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'device_class': 'precipitation', + 'friendly_name': 'La Clusaz Daily precipitation', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_daily_precipitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.3', + }) +# --- +# name: test_sensor[sensor.la_clusaz_freeze_chance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_freeze_chance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:snowflake', + 'original_name': 'La Clusaz Freeze chance', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_freeze_chance', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.la_clusaz_freeze_chance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz Freeze chance', + 'icon': 'mdi:snowflake', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_freeze_chance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.la_clusaz_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_humidity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'La Clusaz Humidity', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.la_clusaz_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'device_class': 'humidity', + 'friendly_name': 'La Clusaz Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensor[sensor.la_clusaz_original_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_original_condition', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'La Clusaz Original condition', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_original_condition', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.la_clusaz_original_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz Original condition', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_original_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Eclaircies', + }) +# --- +# name: test_sensor[sensor.la_clusaz_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_pressure', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'La Clusaz Pressure', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.la_clusaz_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'device_class': 'pressure', + 'friendly_name': 'La Clusaz Pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '988.7', + }) +# --- +# name: test_sensor[sensor.la_clusaz_rain_chance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_rain_chance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-rainy', + 'original_name': 'La Clusaz Rain chance', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_rain_chance', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.la_clusaz_rain_chance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz Rain chance', + 'icon': 'mdi:weather-rainy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_rain_chance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.la_clusaz_snow_chance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_snow_chance', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:weather-snowy', + 'original_name': 'La Clusaz Snow chance', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_snow_chance', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.la_clusaz_snow_chance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz Snow chance', + 'icon': 'mdi:weather-snowy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_snow_chance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor[sensor.la_clusaz_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'La Clusaz Temperature', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.la_clusaz_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'device_class': 'temperature', + 'friendly_name': 'La Clusaz Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.1', + }) +# --- +# name: test_sensor[sensor.la_clusaz_uv-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_uv', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:sunglasses', + 'original_name': 'La Clusaz UV', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_uv', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor[sensor.la_clusaz_uv-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz UV', + 'icon': 'mdi:sunglasses', + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_uv', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensor[sensor.la_clusaz_wind_gust-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_wind_gust', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:weather-windy-variant', + 'original_name': 'La Clusaz Wind gust', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_wind_gust', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.la_clusaz_wind_gust-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'device_class': 'wind_speed', + 'friendly_name': 'La Clusaz Wind gust', + 'icon': 'mdi:weather-windy-variant', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_wind_gust', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65', + }) +# --- +# name: test_sensor[sensor.la_clusaz_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.la_clusaz_wind_speed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'La Clusaz Wind speed', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '45.90417,6.42306_wind_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.la_clusaz_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'device_class': 'wind_speed', + 'friendly_name': 'La Clusaz Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.la_clusaz_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29', + }) +# --- +# name: test_sensor[sensor.meudon_next_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meudon_next_rain', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Meudon Next rain', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '48.807166,2.239895_next_rain', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.meudon_next_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + '1_hour_forecast': dict({ + '0 min': 'Temps sec', + '10 min': 'Temps sec', + '15 min': 'Pluie faible', + '20 min': 'Pluie modérée', + '25 min': 'Pluie faible', + '35 min': 'Temps sec', + '45 min': 'Temps sec', + '5 min': 'Temps sec', + '55 min': 'Temps sec', + }), + 'attribution': 'Data provided by Météo-France', + 'device_class': 'timestamp', + 'forecast_time_ref': '2020-05-20T17:35:00+00:00', + 'friendly_name': 'Meudon Next rain', + }), + 'context': , + 'entity_id': 'sensor.meudon_next_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2020-05-20T17:50:00+00:00', + }) +# --- diff --git a/tests/components/meteo_france/snapshots/test_weather.ambr b/tests/components/meteo_france/snapshots/test_weather.ambr new file mode 100644 index 00000000000..7c64ee86671 --- /dev/null +++ b/tests/components/meteo_france/snapshots/test_weather.ambr @@ -0,0 +1,60 @@ +# serializer version: 1 +# name: test_weather[weather.la_clusaz-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.la_clusaz', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'La Clusaz', + 'platform': 'meteo_france', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '45.90417,6.42306', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather[weather.la_clusaz-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Météo-France', + 'friendly_name': 'La Clusaz', + 'humidity': 75, + 'precipitation_unit': , + 'pressure': 988.7, + 'pressure_unit': , + 'supported_features': , + 'temperature': 9.1, + 'temperature_unit': , + 'visibility_unit': , + 'wind_bearing': 200, + 'wind_speed': 28.8, + 'wind_speed_unit': , + }), + 'context': , + 'entity_id': 'weather.la_clusaz', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'partlycloudy', + }) +# --- diff --git a/tests/components/meteo_france/test_sensor.py b/tests/components/meteo_france/test_sensor.py new file mode 100644 index 00000000000..be77de0008b --- /dev/null +++ b/tests/components/meteo_france/test_sensor.py @@ -0,0 +1,32 @@ +"""Test Météo France weather entity.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.meteo_france.PLATFORMS", [Platform.SENSOR]): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the sensor entity.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/meteo_france/test_weather.py b/tests/components/meteo_france/test_weather.py new file mode 100644 index 00000000000..cd55ac31b27 --- /dev/null +++ b/tests/components/meteo_france/test_weather.py @@ -0,0 +1,31 @@ +"""Test Météo France weather entity.""" + +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.meteo_france.PLATFORMS", [Platform.WEATHER]): + yield + + +async def test_weather( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the weather entity.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 37512ca78f8..8c21fa9cb36 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -7,8 +7,8 @@ from mficlient.client import FailedToLogin import pytest import requests -import homeassistant.components.mfi.sensor as mfi -import homeassistant.components.sensor as sensor_component +from homeassistant.components import sensor as sensor_component +from homeassistant.components.mfi import sensor as mfi from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 03b5d5f2c0a..fb586073a3d 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -4,8 +4,8 @@ from unittest import mock import pytest -import homeassistant.components.mfi.switch as mfi -import homeassistant.components.switch as switch_component +from homeassistant.components import switch as switch_component +from homeassistant.components.mfi import switch as mfi from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py index bf0026b8c6c..a47e6422bf8 100644 --- a/tests/components/mill/test_init.py +++ b/tests/components/mill/test_init.py @@ -147,5 +147,5 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) - assert unload_entry.call_count == 2 + assert unload_entry.call_count == 3 assert entry.entry_id not in hass.data[mill.DOMAIN] diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index c875697bf2f..a7a70043d94 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import get_fixture_path diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 9fea3b5c14c..d7875246fac 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -9,6 +9,8 @@ from homeassistant.components.mochad import switch as mochad from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockEntityPlatform + @pytest.fixture(autouse=True) def pymochad_mock(): @@ -25,7 +27,9 @@ def switch_mock(hass: HomeAssistant) -> mochad.MochadSwitch: """Mock switch.""" controller_mock = mock.MagicMock() dev_dict = {"address": "a1", "name": "fake_switch"} - return mochad.MochadSwitch(hass, controller_mock, dev_dict) + entity = mochad.MochadSwitch(hass, controller_mock, dev_dict) + entity.platform = MockEntityPlatform(hass) + return entity async def test_setup_adds_proper_devices(hass: HomeAssistant) -> None: diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index cdea046ceea..0a2cbf44b9e 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -22,7 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, mock_restore_cache diff --git a/tests/components/modbus/fixtures/configuration_2.yaml b/tests/components/modbus/fixtures/configuration_2.yaml new file mode 100644 index 00000000000..3f7b062c4cb --- /dev/null +++ b/tests/components/modbus/fixtures/configuration_2.yaml @@ -0,0 +1,12 @@ +modbus: + type: "tcp" + host: "testHost" + port: 5001 + name: "testModbus" + sensors: + - name: "dummy" + address: 117 + slave: 0 + - name: "dummy_2" + address: 118 + slave: 1 diff --git a/tests/components/modbus/fixtures/configuration_empty.yaml b/tests/components/modbus/fixtures/configuration_empty.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 24293377174..e1c0e08a113 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -422,9 +422,9 @@ async def test_virtual_binary_sensor( assert hass.states.get(ENTITY_ID).state == expected for i, slave in enumerate(slaves): - entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i + 1}".replace(" ", "_") assert hass.states.get(entity_id).state == slave - unique_id = f"{SLAVE_UNIQUE_ID}_{i+1}" + unique_id = f"{SLAVE_UNIQUE_ID}_{i + 1}" entry = entity_registry.async_get(entity_id) assert entry.unique_id == unique_id diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index d34846639b5..3c30efe9dce 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -24,6 +24,8 @@ from homeassistant.components.climate import ( SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, @@ -54,6 +56,9 @@ from homeassistant.components.modbus.const import ( CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, + CONF_HVAC_OFF_VALUE, + CONF_HVAC_ON_VALUE, + CONF_HVAC_ONOFF_COIL, CONF_HVAC_ONOFF_REGISTER, CONF_MAX_TEMP, CONF_MIN_TEMP, @@ -362,6 +367,109 @@ async def test_config_hvac_onoff_register(hass: HomeAssistant, mock_modbus) -> N assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES] +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_ONOFF_REGISTER: 11, + } + ], + }, + ], +) +async def test_config_hvac_onoff_coil(hass: HomeAssistant, mock_modbus) -> None: + """Run configuration test for On/Off coil.""" + state = hass.states.get(ENTITY_ID) + assert HVACMode.OFF in state.attributes[ATTR_HVAC_MODES] + assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES] + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_ONOFF_REGISTER: 11, + CONF_HVAC_ON_VALUE: 0xAA, + CONF_HVAC_OFF_VALUE: 0xFF, + } + ], + }, + ], +) +async def test_hvac_onoff_values(hass: HomeAssistant, mock_modbus) -> None: + """Run configuration test for On/Off register values.""" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_modbus.write_register.assert_called_with(11, value=0xAA, slave=10) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_modbus.write_register.assert_called_with(11, value=0xFF, slave=10) + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_HVAC_ONOFF_COIL: 11, + } + ], + }, + ], +) +async def test_hvac_onoff_coil(hass: HomeAssistant, mock_modbus) -> None: + """Run configuration test for On/Off coil values.""" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_modbus.write_coil.assert_called_with(11, value=1, slave=10) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_modbus.write_coil.assert_called_with(11, value=0, slave=10) + + @pytest.mark.parametrize( "do_config", [ @@ -517,6 +625,126 @@ async def test_service_climate_update( assert hass.states.get(ENTITY_ID).state == result +@pytest.mark.parametrize( + ("do_config", "result", "register_words", "coil_value"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: [130, 131, 132, 133, 134, 135, 136], + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_DRY: 2, + }, + }, + CONF_HVAC_ONOFF_COIL: 11, + }, + ] + }, + HVACMode.COOL, + [0x00], + [0x01], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 119, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_DRY: 2, + }, + }, + CONF_HVAC_ONOFF_COIL: 11, + }, + ] + }, + HVACMode.HEAT, + [0x01], + [0x01], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 2, + CONF_HVAC_MODE_DRY: 3, + }, + }, + CONF_HVAC_ONOFF_COIL: 11, + }, + ] + }, + HVACMode.OFF, + [0x00], + [0x00], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_HVAC_ONOFF_COIL: 11, + }, + ] + }, + "unavailable", + [0x00], + None, + ), + ], +) +async def test_hvac_onoff_coil_update( + hass: HomeAssistant, mock_modbus_ha, result, register_words, coil_value +) -> None: + """Test climate update based on On/Off coil values.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) + mock_modbus_ha.read_coils.return_value = ReadResult(coil_value) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == result + + @pytest.mark.parametrize( ("do_config", "result", "register_words"), [ diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 0cfa7ba8b24..7b76dbc3528 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -25,7 +25,6 @@ import voluptuous as vol from homeassistant import config as hass_config from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.modbus import async_reset_platform from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, @@ -108,7 +107,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( TEST_ENTITY_NAME, @@ -847,6 +846,13 @@ async def test_pb_service_write( CALL_TYPE_WRITE_REGISTERS: mock_modbus_with_pymodbus.write_registers, } + value_arg_name = { + CALL_TYPE_WRITE_COIL: "value", + CALL_TYPE_WRITE_COILS: "values", + CALL_TYPE_WRITE_REGISTER: "value", + CALL_TYPE_WRITE_REGISTERS: "values", + } + data = { ATTR_HUB: TEST_MODBUS_NAME, do_slave: 17, @@ -859,10 +865,12 @@ async def test_pb_service_write( func_name[do_write[FUNC]].return_value = do_return[VALUE] await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) assert func_name[do_write[FUNC]].called - assert func_name[do_write[FUNC]].call_args[0] == ( - data[ATTR_ADDRESS], - data[do_write[DATA]], - ) + assert func_name[do_write[FUNC]].call_args.args == (data[ATTR_ADDRESS],) + assert func_name[do_write[FUNC]].call_args.kwargs == { + "slave": 17, + value_arg_name[do_write[FUNC]]: data[do_write[DATA]], + } + if do_return[DATA]: assert any(message.startswith("Pymodbus:") for message in caplog.messages) @@ -1159,22 +1167,61 @@ async def test_integration_reload( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus, - freezer: FrozenDateTimeFactory, ) -> None: """Run test for integration reload.""" caplog.set_level(logging.DEBUG) caplog.clear() - yaml_path = get_fixture_path("configuration.yaml", "modbus") + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + yaml_path = get_fixture_path("configuration.yaml", DOMAIN) with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): - await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) await hass.async_block_till_done() - for _ in range(4): - freezer.tick(timedelta(seconds=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() assert "Modbus reloading" in caplog.text + state_sensor_1 = hass.states.get("sensor.dummy") + state_sensor_2 = hass.states.get("sensor.dummy_2") + assert state_sensor_1 + assert not state_sensor_2 + + caplog.clear() + yaml_path = get_fixture_path("configuration_2.yaml", DOMAIN) + with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + assert "Modbus reloading" in caplog.text + state_sensor_1 = hass.states.get("sensor.dummy") + state_sensor_2 = hass.states.get("sensor.dummy_2") + assert state_sensor_1 + assert state_sensor_2 + + caplog.clear() + yaml_path = get_fixture_path("configuration_empty.yaml", DOMAIN) + with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + assert "Modbus not present anymore" in caplog.text + state_sensor_1 = hass.states.get("sensor.dummy") + state_sensor_2 = hass.states.get("sensor.dummy_2") + assert not state_sensor_1 + assert not state_sensor_2 @pytest.mark.parametrize("do_config", [{}]) @@ -1229,7 +1276,54 @@ async def test_no_entities(hass: HomeAssistant) -> None: assert await async_setup_component(hass, DOMAIN, config) is False -async def test_reset_platform(hass: HomeAssistant) -> None: - """Run test for async_reset_platform.""" - await async_reset_platform(hass, "modbus") - assert DOMAIN not in hass.data +@pytest.mark.parametrize( + ("do_config", "expected_slave_value"), + [ + ( + { + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 1234, + }, + ], + }, + 1, + ), + ( + { + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 1234, + CONF_SLAVE: 0, + }, + ], + }, + 0, + ), + ( + { + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 1234, + CONF_DEVICE_ADDRESS: 6, + }, + ], + }, + 6, + ), + ], +) +async def test_check_default_slave( + hass: HomeAssistant, + mock_modbus, + do_config, + mock_do_cycle, + expected_slave_value: int, +) -> None: + """Test default slave.""" + assert mock_modbus.read_holding_registers.mock_calls + first_call = mock_modbus.read_holding_registers.mock_calls[0] + assert first_call.kwargs["slave"] == expected_slave_value diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 4e0ad0841ea..4b2c123ba75 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -41,7 +41,7 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import TEST_ENTITY_NAME, ReadResult diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 2ae4d6659e7..280deadc733 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -10,10 +10,11 @@ from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_DEVICE, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.usb import UsbServiceInfo from . import com_port, patch_config_flow_modem -DISCOVERY_INFO = usb.UsbServiceInfo( +DISCOVERY_INFO = UsbServiceInfo( device=phone_modem.DEFAULT_PORT, pid="1340", vid="0572", diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 5b10d4d729e..4ec5e92cd72 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -6,12 +6,12 @@ from unittest.mock import MagicMock, patch import aiohttp from aiomodernforms import ModernFormsConnectionError -from homeassistant.components import zeroconf from homeassistant.components.modern_forms.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from . import init_integration @@ -66,7 +66,7 @@ async def test_full_zeroconf_flow_implementation( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -138,7 +138,7 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -170,7 +170,7 @@ async def test_zeroconf_confirm_connection_error( CONF_HOST: "example.com", CONF_NAME: "test", }, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.com.", @@ -220,7 +220,7 @@ async def test_zeroconf_with_mac_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr index dc6680ff99a..461cb33d776 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr index 7dfb9edb2e8..27244d781df 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr index c1a63271a33..0708137e1cf 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr @@ -19,6 +19,7 @@ 'target_temp_step': 0.2, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr index 3fee26a6ed5..4b1c702591d 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/monarch_money/snapshots/test_sensor.ambr b/tests/components/monarch_money/snapshots/test_sensor.ambr index cf7e0cb7b2f..b70302188ed 100644 --- a/tests/components/monarch_money/snapshots/test_sensor.ambr +++ b/tests/components/monarch_money/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -108,6 +110,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -160,6 +163,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -211,6 +215,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -261,6 +266,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -311,6 +317,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -362,6 +369,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -412,6 +420,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -463,6 +472,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -513,6 +523,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -564,6 +575,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -614,6 +626,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -665,6 +678,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -715,6 +729,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -766,6 +781,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -816,6 +832,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -867,6 +884,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -915,6 +933,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -965,6 +984,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1018,6 +1038,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1069,6 +1090,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/monzo/snapshots/test_sensor.ambr b/tests/components/monzo/snapshots/test_sensor.ambr index 9be5943d35c..8d3f83ed4f1 100644 --- a/tests/components/monzo/snapshots/test_sensor.ambr +++ b/tests/components/monzo/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -162,6 +165,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -214,6 +218,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py index 7a341052f22..d2887451629 100644 --- a/tests/components/mopeka/test_config_flow.py +++ b/tests/components/mopeka/test_config_flow.py @@ -81,6 +81,39 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=PRO_SERVICE_INFO.address, + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Pro Plus EEFF" + assert CONF_MEDIUM_TYPE in result2["data"] + assert result2["data"][CONF_MEDIUM_TYPE] in [ + medium_type.value for medium_type in MediumType + ] + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 77171b06ad6..821e4fa0278 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -6,12 +6,12 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.motion_blinds import const from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -346,7 +346,7 @@ async def test_config_flow_invalid_interface(hass: HomeAssistant) -> None: async def test_dhcp_flow(hass: HomeAssistant) -> None: """Successful flow from DHCP discovery.""" - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST, hostname="MOTION_abcdef", macaddress=DHCP_FORMATTED_MAC, @@ -380,7 +380,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: async def test_dhcp_flow_abort(hass: HomeAssistant) -> None: """Test that DHCP discovery aborts if not Motionblinds.""" - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST, hostname="MOTION_abcdef", macaddress=DHCP_FORMATTED_MAC, @@ -400,7 +400,7 @@ async def test_dhcp_flow_abort(hass: HomeAssistant) -> None: async def test_dhcp_flow_abort_invalid_response(hass: HomeAssistant) -> None: """Test that DHCP discovery aborts if device responded with invalid data.""" - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST, hostname="MOTION_abcdef", macaddress=DHCP_FORMATTED_MAC, diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index 842d862a222..c403f9f072f 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c" -TEST_URL = f"http://test:{DEFAULT_PORT+1}" +TEST_URL = f"http://test:{DEFAULT_PORT + 1}" TEST_CAMERA_ID = 100 TEST_CAMERA_NAME = "Test Camera" TEST_CAMERA_ENTITY_ID = "camera.test_camera" diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 8ef58cc968d..d9a9a847b63 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -45,8 +45,8 @@ from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util from homeassistant.util.aiohttp import MockRequest -import homeassistant.util.dt as dt_util from . import ( TEST_CAMERA, diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index fae7fccbb6d..bc345c0b66f 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -31,7 +31,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import ( TEST_CAMERA, diff --git a/tests/components/motionmount/__init__.py b/tests/components/motionmount/__init__.py index da6fbae32a3..3b97c8aa7fe 100644 --- a/tests/components/motionmount/__init__.py +++ b/tests/components/motionmount/__init__.py @@ -2,8 +2,8 @@ from ipaddress import ip_address -from homeassistant.components import zeroconf -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo HOST = "192.168.1.31" PORT = 23 @@ -21,7 +21,9 @@ MOCK_USER_INPUT = { CONF_PORT: PORT, } -MOCK_ZEROCONF_TVM_SERVICE_INFO_V1 = zeroconf.ZeroconfServiceInfo( +MOCK_PIN_INPUT = {CONF_PIN: 1234} + +MOCK_ZEROCONF_TVM_SERVICE_INFO_V1 = ZeroconfServiceInfo( type=TVM_ZEROCONF_SERVICE_TYPE, name=f"{ZEROCONF_NAME}.{TVM_ZEROCONF_SERVICE_TYPE}", ip_address=ip_address(ZEROCONF_HOST), @@ -31,7 +33,7 @@ MOCK_ZEROCONF_TVM_SERVICE_INFO_V1 = zeroconf.ZeroconfServiceInfo( properties={"txtvers": "1", "model": "TVM 7675"}, ) -MOCK_ZEROCONF_TVM_SERVICE_INFO_V2 = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_TVM_SERVICE_INFO_V2 = ZeroconfServiceInfo( type=TVM_ZEROCONF_SERVICE_TYPE, name=f"{ZEROCONF_NAME}.{TVM_ZEROCONF_SERVICE_TYPE}", ip_address=ip_address(ZEROCONF_HOST), diff --git a/tests/components/motionmount/test_config_flow.py b/tests/components/motionmount/test_config_flow.py index 4de23de63c9..1fa2715595d 100644 --- a/tests/components/motionmount/test_config_flow.py +++ b/tests/components/motionmount/test_config_flow.py @@ -1,20 +1,23 @@ """Tests for the Vogel's MotionMount config flow.""" import dataclasses +from datetime import timedelta import socket from unittest.mock import MagicMock, PropertyMock +from freezegun.api import FrozenDateTimeFactory import motionmount import pytest from homeassistant.components.motionmount.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( HOST, + MOCK_PIN_INPUT, MOCK_USER_INPUT, MOCK_ZEROCONF_TVM_SERVICE_INFO_V1, MOCK_ZEROCONF_TVM_SERVICE_INFO_V2, @@ -24,23 +27,12 @@ from . import ( ZEROCONF_NAME, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed MAC = bytes.fromhex("c4dd57f8a55f") pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_show_user_form(hass: HomeAssistant) -> None: - """Test that the user set up form is served.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result["step_id"] == "user" - assert result["type"] is FlowResultType.FORM - - async def test_user_connection_error( hass: HomeAssistant, mock_motionmount_config_flow: MagicMock, @@ -117,33 +109,6 @@ async def test_user_not_connected_error( assert result["reason"] == "not_connected" -async def test_user_response_error_single_device_old_ce_old_new_pro( - hass: HomeAssistant, - mock_motionmount_config_flow: MagicMock, -) -> None: - """Test that the flow creates an entry when there is a response error.""" - mock_motionmount_config_flow.connect.side_effect = ( - motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound) - ) - - user_input = MOCK_USER_INPUT.copy() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=user_input, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == HOST - - assert result["data"] - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - - assert result["result"] - - async def test_user_response_error_single_device_new_ce_old_pro( hass: HomeAssistant, mock_motionmount_config_flow: MagicMock, @@ -199,30 +164,6 @@ async def test_user_response_error_single_device_new_ce_new_pro( assert result["result"].unique_id == ZEROCONF_MAC -async def test_user_response_error_multi_device_old_ce_old_new_pro( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_motionmount_config_flow: MagicMock, -) -> None: - """Test that the flow is aborted when there are multiple devices.""" - mock_config_entry.add_to_hass(hass) - - mock_motionmount_config_flow.connect.side_effect = ( - motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound) - ) - - user_input = MOCK_USER_INPUT.copy() - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=user_input, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - async def test_user_response_error_multi_device_new_ce_new_pro( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -246,6 +187,53 @@ async def test_user_response_error_multi_device_new_ce_new_pro( assert result["reason"] == "already_configured" +async def test_user_response_authentication_needed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + + user_input = MOCK_USER_INPUT.copy() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + # Now simulate the user entered the correct pin to finalize the test + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + async def test_zeroconf_connection_error( hass: HomeAssistant, mock_motionmount_config_flow: MagicMock, @@ -322,48 +310,6 @@ async def test_zeroconf_not_connected_error( assert result["reason"] == "not_connected" -async def test_show_zeroconf_form_old_ce_old_pro( - hass: HomeAssistant, - mock_motionmount_config_flow: MagicMock, -) -> None: - """Test that the zeroconf confirmation form is served.""" - mock_motionmount_config_flow.connect.side_effect = ( - motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound) - ) - - discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V1) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data=discovery_info, - ) - - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] is FlowResultType.FORM - assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} - - -async def test_show_zeroconf_form_old_ce_new_pro( - hass: HomeAssistant, - mock_motionmount_config_flow: MagicMock, -) -> None: - """Test that the zeroconf confirmation form is served.""" - mock_motionmount_config_flow.connect.side_effect = ( - motionmount.MotionMountResponseError(motionmount.MotionMountResponse.NotFound) - ) - - discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data=discovery_info, - ) - - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] is FlowResultType.FORM - assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} - - async def test_show_zeroconf_form_new_ce_old_pro( hass: HomeAssistant, mock_motionmount_config_flow: MagicMock, @@ -384,6 +330,21 @@ async def test_show_zeroconf_form_new_ce_old_pro( assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME + assert result["data"][CONF_PORT] == PORT + assert result["data"][CONF_NAME] == ZEROCONF_NAME + + assert result["result"] + assert result["result"].unique_id is None + async def test_show_zeroconf_form_new_ce_new_pro( hass: HomeAssistant, @@ -403,6 +364,21 @@ async def test_show_zeroconf_form_new_ce_new_pro( assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "My MotionMount"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME + assert result["data"][CONF_PORT] == PORT + assert result["data"][CONF_NAME] == ZEROCONF_NAME + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + async def test_zeroconf_device_exists_abort( hass: HomeAssistant, @@ -423,6 +399,346 @@ async def test_zeroconf_device_exists_abort( assert result["reason"] == "already_configured" +async def test_zeroconf_authentication_needed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + + discovery_info = dataclasses.replace(MOCK_ZEROCONF_TVM_SERVICE_INFO_V2) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + # Now simulate the user entered the correct pin to finalize the test + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == ZEROCONF_HOSTNAME + assert result["data"][CONF_PORT] == PORT + assert result["data"][CONF_NAME] == ZEROCONF_NAME + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + +async def test_authentication_incorrect_then_correct_pin( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + user_input = MOCK_USER_INPUT.copy() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + assert result["errors"] + assert result["errors"][CONF_PIN] == CONF_PIN + + # Now simulate the user entered the correct pin to finalize the test + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + +async def test_authentication_first_incorrect_pin_to_backoff( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + side_effect=[True, 1] + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert mock_motionmount_config_flow.authenticate.called + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backoff" + + freezer.tick(timedelta(seconds=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Now simulate the user entered the correct pin to finalize the test + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + +async def test_authentication_multiple_incorrect_pins( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock(return_value=1) + + user_input = MOCK_USER_INPUT.copy() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backoff" + + freezer.tick(timedelta(seconds=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Now simulate the user entered the correct pin to finalize the test + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + +async def test_authentication_show_backoff_when_still_running( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock(return_value=1) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backoff" + + # This situation happens when the user cancels the progress dialog and tries to + # configure the MotionMount again + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=None, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backoff" + + freezer.tick(timedelta(seconds=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Now simulate the user entered the correct pin to finalize the test + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + +async def test_authentication_correct_pin( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, +) -> None: + """Test that authentication is requested when needed.""" + type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) + type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=False + ) + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + + user_input = MOCK_USER_INPUT.copy() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == ZEROCONF_NAME + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + assert result["result"] + assert result["result"].unique_id == ZEROCONF_MAC + + async def test_full_user_flow_implementation( hass: HomeAssistant, mock_motionmount_config_flow: MagicMock, @@ -459,7 +775,7 @@ async def test_full_zeroconf_flow_implementation( hass: HomeAssistant, mock_motionmount_config_flow: MagicMock, ) -> None: - """Test the full manual user flow from start to finish.""" + """Test the full zeroconf flow from start to finish.""" type(mock_motionmount_config_flow).name = PropertyMock(return_value=ZEROCONF_NAME) type(mock_motionmount_config_flow).mac = PropertyMock(return_value=MAC) @@ -487,3 +803,37 @@ async def test_full_zeroconf_flow_implementation( assert result["result"] assert result["result"].unique_id == ZEROCONF_MAC + + +async def test_full_reauth_flow_implementation( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_motionmount_config_flow: MagicMock, +) -> None: + """Test reauthentication.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + type(mock_motionmount_config_flow).can_authenticate = PropertyMock( + return_value=True + ) + type(mock_motionmount_config_flow).is_authenticated = PropertyMock( + return_value=True + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_PIN_INPUT.copy(), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/motionmount/test_sensor.py b/tests/components/motionmount/test_sensor.py new file mode 100644 index 00000000000..0320e62d640 --- /dev/null +++ b/tests/components/motionmount/test_sensor.py @@ -0,0 +1,49 @@ +"""Tests for the MotionMount Sensor platform.""" + +from unittest.mock import patch + +from motionmount import MotionMountSystemError +import pytest + +from homeassistant.core import HomeAssistant + +from . import ZEROCONF_NAME + +from tests.common import MockConfigEntry + +MAC = bytes.fromhex("c4dd57f8a55f") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("system_status", "state"), + [ + (None, "none"), + (MotionMountSystemError.MotorError, "motor"), + (MotionMountSystemError.ObstructionDetected, "obstruction"), + (MotionMountSystemError.TVWidthConstraintError, "tv_width_constraint"), + (MotionMountSystemError.HDMICECError, "hdmi_cec"), + (MotionMountSystemError.InternalError, "internal"), + ], +) +async def test_error_status_sensor_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + system_status: MotionMountSystemError, + state: str, +) -> None: + """Tests the state attributes.""" + with patch( + "homeassistant.components.motionmount.motionmount.MotionMount", + autospec=True, + ) as motionmount_mock: + motionmount_mock.return_value.name = ZEROCONF_NAME + motionmount_mock.return_value.mac = MAC + motionmount_mock.return_value.is_authenticated = True + motionmount_mock.return_value.system_status = [system_status] + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert hass.states.get("sensor.my_motionmount_error_status").state == state diff --git a/tests/components/mpd/test_config_flow.py b/tests/components/mpd/test_config_flow.py index d17bef60446..af2ba7562a8 100644 --- a/tests/components/mpd/test_config_flow.py +++ b/tests/components/mpd/test_config_flow.py @@ -7,8 +7,8 @@ import mpd import pytest from homeassistant.components.mpd.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -107,85 +107,3 @@ async def test_existing_entry( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_full_import_flow( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_mpd_client: AsyncMock, -) -> None: - """Test the happy flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: "192.168.0.1", - CONF_PORT: 6600, - CONF_PASSWORD: "test123", - CONF_NAME: "My PC", - }, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "My PC" - assert result["data"] == { - CONF_HOST: "192.168.0.1", - CONF_PORT: 6600, - CONF_PASSWORD: "test123", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("exception", "error"), - [ - (TimeoutError, "cannot_connect"), - (gaierror, "cannot_connect"), - (mpd.ConnectionError, "cannot_connect"), - (OSError, "cannot_connect"), - (Exception, "unknown"), - ], -) -async def test_import_errors( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_mpd_client: AsyncMock, - exception: Exception, - error: str, -) -> None: - """Test we handle errors correctly.""" - mock_mpd_client.password.side_effect = exception - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: "192.168.0.1", - CONF_PORT: 6600, - CONF_PASSWORD: "test123", - CONF_NAME: "My PC", - }, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == error - - -async def test_existing_entry_import( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test we abort if an entry already exists.""" - mock_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: "192.168.0.1", - CONF_PORT: 6600, - CONF_PASSWORD: "test123", - CONF_NAME: "My PC", - }, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 22f0416a2c6..87bbcecebe5 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -18,7 +18,6 @@ from tests.common import MockConfigEntry from tests.typing import MqttMockPahoClient ENTRY_DEFAULT_BIRTH_MESSAGE = { - mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: { mqtt.ATTR_TOPIC: "homeassistant/status", mqtt.ATTR_PAYLOAD: "online", @@ -39,7 +38,7 @@ def temp_dir_prefix() -> str: return "test" -@pytest.fixture +@pytest.fixture(autouse=True) def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: """Mock the certificate temp directory.""" with patch( @@ -77,6 +76,7 @@ def mock_debouncer(hass: HomeAssistant) -> Generator[asyncio.Event]: async def setup_with_birth_msg_client_mock( hass: HomeAssistant, mqtt_config_entry_data: dict[str, Any] | None, + mqtt_config_entry_options: dict[str, Any] | None, mqtt_client_mock: MqttMockPahoClient, ) -> AsyncGenerator[MqttMockPahoClient]: """Test sending birth message.""" @@ -89,6 +89,9 @@ async def setup_with_birth_msg_client_mock( entry = MockConfigEntry( domain=mqtt.DOMAIN, data=mqtt_config_entry_data or {mqtt.CONF_BROKER: "test-broker"}, + options=mqtt_config_entry_options or {}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index d27163c3423..34be237fb72 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .test_common import ( help_custom_config, diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index 4bfcde752ae..b526d70490b 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -32,6 +32,7 @@ from .test_common import help_all_subscribe_calls from tests.common import ( MockConfigEntry, + MockMqttReasonCode, async_fire_mqtt_message, async_fire_time_changed, ) @@ -94,7 +95,7 @@ async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: mqtt_client.connect = MagicMock( return_value=0, side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mqtt_client.on_connect, mqtt_client, None, 0, 0, 0 + mqtt_client.on_connect, mqtt_client, None, 0, MockMqttReasonCode() ), ) mqtt_client.publish = MagicMock(return_value=FakeInfo()) @@ -105,6 +106,8 @@ async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: mqtt.CONF_BROKER: "test-broker", mqtt.CONF_DISCOVERY: False, }, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) @@ -117,7 +120,7 @@ async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: ) await asyncio.sleep(0) # Simulate late ACK callback from client with mid 100 - mqtt_client.on_publish(0, 0, 100) + mqtt_client.on_publish(0, 0, 100, MockMqttReasonCode(), None) # disconnect the MQTT client await hass.async_stop() await hass.async_block_till_done() @@ -132,7 +135,7 @@ async def test_mqtt_await_ack_at_disconnect(hass: HomeAssistant) -> None: await hass.async_block_till_done(wait_background_tasks=True) -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@pytest.mark.parametrize("mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_publish( hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient ) -> None: @@ -776,10 +779,10 @@ async def test_replaying_payload_same_topic( calls_a = [] calls_b = [] mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() # Simulate a (retained) message played back after reconnecting @@ -906,10 +909,10 @@ async def test_replaying_payload_wildcard_topic( calls_a = [] calls_b = [] mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) await mock_debouncer.wait() mqtt_client_mock.subscribe.assert_called() @@ -1022,8 +1025,8 @@ async def test_unsubscribe_race( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_DISCOVERY: False})], ) async def test_restore_subscriptions_on_reconnect( hass: HomeAssistant, @@ -1043,7 +1046,7 @@ async def test_restore_subscriptions_on_reconnect( assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) mqtt_client_mock.reset_mock() - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) # Test to subscribe orther topic while the client is not connected await mqtt.async_subscribe(hass, "test/other", record_calls) @@ -1051,7 +1054,7 @@ async def test_restore_subscriptions_on_reconnect( assert ("test/other", 0) not in help_all_subscribe_calls(mqtt_client_mock) mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) await mock_debouncer.wait() # Assert all subscriptions are performed at the broker assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) @@ -1059,8 +1062,8 @@ async def test_restore_subscriptions_on_reconnect( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_DISCOVERY: False})], ) async def test_restore_all_active_subscriptions_on_reconnect( hass: HomeAssistant, @@ -1087,10 +1090,10 @@ async def test_restore_all_active_subscriptions_on_reconnect( unsub() assert mqtt_client_mock.unsubscribe.call_count == 0 - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) # wait for cooldown await mock_debouncer.wait() @@ -1100,8 +1103,8 @@ async def test_restore_all_active_subscriptions_on_reconnect( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_DISCOVERY: False})], ) async def test_subscribed_at_highest_qos( hass: HomeAssistant, @@ -1136,7 +1139,12 @@ async def test_initial_setup_logs_error( mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test for setup failure if initial client connection fails.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) mqtt_client_mock.connect.side_effect = MagicMock(return_value=1) try: @@ -1153,27 +1161,37 @@ async def test_logs_error_if_no_connect_broker( ) -> None: """Test for setup failure if connection to broker is missing.""" mqtt_client_mock = setup_with_birth_msg_client_mock - # test with rc = 3 -> broker unavailable - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, 3) - await hass.async_block_till_done() - assert ( - "Unable to connect to the MQTT broker: Connection Refused: broker unavailable." - in caplog.text + # test with reason code = 136 -> server unavailable + mqtt_client_mock.on_disconnect(Mock(), None, None, MockMqttReasonCode()) + mqtt_client_mock.on_connect( + Mock(), + None, + None, + MockMqttReasonCode(value=136, is_failure=True, name="Server unavailable"), ) + await hass.async_block_till_done() + assert "Unable to connect to the MQTT broker: Server unavailable" in caplog.text -@pytest.mark.parametrize("return_code", [4, 5]) +@pytest.mark.parametrize( + "reason_code", + [ + MockMqttReasonCode( + value=134, is_failure=True, name="Bad user name or password" + ), + MockMqttReasonCode(value=135, is_failure=True, name="Not authorized"), + ], +) async def test_triggers_reauth_flow_if_auth_fails( hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient, - return_code: int, + reason_code: MockMqttReasonCode, ) -> None: """Test re-auth is triggered if authentication is failing.""" mqtt_client_mock = setup_with_birth_msg_client_mock # test with rc = 4 -> CONNACK_REFUSED_NOT_AUTHORIZED and 5 -> CONNACK_REFUSED_BAD_USERNAME_PASSWORD - mqtt_client_mock.on_disconnect(Mock(), None, 0) - mqtt_client_mock.on_connect(Mock(), None, None, return_code) + mqtt_client_mock.on_disconnect(Mock(), None, 0, MockMqttReasonCode(), None) + mqtt_client_mock.on_connect(Mock(), None, None, reason_code) await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -1190,7 +1208,9 @@ async def test_handle_mqtt_on_callback( mqtt_client_mock = setup_with_birth_msg_client_mock with patch.object(mqtt_client_mock, "get_mid", return_value=100): # Simulate an ACK for mid == 100, this will call mqtt_mock._async_get_mid_future(mid) - mqtt_client_mock.on_publish(mqtt_client_mock, None, 100) + mqtt_client_mock.on_publish( + mqtt_client_mock, None, 100, MockMqttReasonCode(), None + ) await hass.async_block_till_done() # Make sure the ACK has been received await hass.async_block_till_done() @@ -1212,7 +1232,7 @@ async def test_handle_mqtt_on_callback_after_cancellation( # Simulate the mid future getting a cancellation mqtt_mock()._async_get_mid_future(101).cancel() # Simulate an ACK for mid == 101, being received after the cancellation - mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101, MockMqttReasonCode(), None) await hass.async_block_till_done() assert "No ACK from MQTT server" not in caplog.text assert "InvalidStateError" not in caplog.text @@ -1229,7 +1249,7 @@ async def test_handle_mqtt_on_callback_after_timeout( # Simulate the mid future getting a timeout mqtt_mock()._async_get_mid_future(101).set_exception(asyncio.TimeoutError) # Simulate an ACK for mid == 101, being received after the timeout - mqtt_client_mock.on_publish(mqtt_client_mock, None, 101) + mqtt_client_mock.on_publish(mqtt_client_mock, None, 101, MockMqttReasonCode(), None) await hass.async_block_till_done() assert "No ACK from MQTT server" not in caplog.text assert "InvalidStateError" not in caplog.text @@ -1239,7 +1259,12 @@ async def test_publish_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test publish error.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) # simulate an Out of memory error @@ -1376,12 +1401,15 @@ async def test_handle_mqtt_timeout_on_callback( mock_client.connect = MagicMock( return_value=0, side_effect=lambda *args, **kwargs: hass.loop.call_soon_threadsafe( - mock_client.on_connect, mock_client, None, 0, 0, 0 + mock_client.on_connect, mock_client, None, 0, MockMqttReasonCode() ), ) entry = MockConfigEntry( - domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) @@ -1403,17 +1431,29 @@ async def test_handle_mqtt_timeout_on_callback( assert not mock_debouncer.is_set() +@pytest.mark.parametrize( + "exception", + [ + OSError("Connection error"), + paho_mqtt.WebsocketConnectionError("Connection error"), + ], +) async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, exception: Exception ) -> None: """Test for setup failure if connection to broker is missing.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) with patch( "homeassistant.components.mqtt.async_client.AsyncMQTTClient" ) as mock_client: - mock_client().connect = MagicMock(side_effect=OSError("Connection error")) + mock_client().connect = MagicMock(side_effect=exception) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert "Failed to connect to MQTT server due to exception:" in caplog.text @@ -1488,17 +1528,19 @@ async def test_tls_version( @pytest.mark.parametrize( - "mqtt_config_entry_data", + ("mqtt_config_entry_data", "mqtt_config_entry_options"), [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "birth", - mqtt.ATTR_PAYLOAD: "birth", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, + ( + {mqtt.CONF_BROKER: "mock-broker"}, + { + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "birth", + mqtt.ATTR_PAYLOAD: "birth", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + } }, - } + ) ], ) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @@ -1508,11 +1550,18 @@ async def test_custom_birth_message( hass: HomeAssistant, mock_debouncer: asyncio.Event, mqtt_config_entry_data: dict[str, Any], + mqtt_config_entry_options: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=mqtt_config_entry_data, + options=mqtt_config_entry_options, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) @@ -1526,7 +1575,7 @@ async def test_custom_birth_message( @pytest.mark.parametrize( - "mqtt_config_entry_data", + "mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_default_birth_message( @@ -1541,8 +1590,8 @@ async def test_default_birth_message( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_BIRTH_MESSAGE: {}})], ) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @@ -1552,10 +1601,17 @@ async def test_no_birth_message( record_calls: MessageCallbackType, mock_debouncer: asyncio.Event, mqtt_config_entry_data: dict[str, Any], + mqtt_config_entry_options: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test disabling birth message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=mqtt_config_entry_data, + options=mqtt_config_entry_options, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) mock_debouncer.clear() @@ -1575,20 +1631,27 @@ async def test_no_birth_message( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [ENTRY_DEFAULT_BIRTH_MESSAGE], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, ENTRY_DEFAULT_BIRTH_MESSAGE)], ) @patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.2) async def test_delayed_birth_message( hass: HomeAssistant, mqtt_config_entry_data: dict[str, Any], + mqtt_config_entry_options: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test sending birth message does not happen until Home Assistant starts.""" hass.set_state(CoreState.starting) await hass.async_block_till_done() birth = asyncio.Event() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=mqtt_config_entry_data, + options=mqtt_config_entry_options, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) @@ -1612,7 +1675,7 @@ async def test_delayed_birth_message( @pytest.mark.parametrize( - "mqtt_config_entry_data", + "mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_subscription_done_when_birth_message_is_sent( @@ -1630,26 +1693,37 @@ async def test_subscription_done_when_birth_message_is_sent( @pytest.mark.parametrize( - "mqtt_config_entry_data", + ("mqtt_config_entry_data", "mqtt_config_entry_options"), [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "death", - mqtt.ATTR_PAYLOAD: "death", - mqtt.ATTR_QOS: 0, - mqtt.ATTR_RETAIN: False, + ( + { + mqtt.CONF_BROKER: "mock-broker", }, - } + { + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "death", + mqtt.ATTR_PAYLOAD: "death", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + }, + ) ], ) async def test_custom_will_message( hass: HomeAssistant, mqtt_config_entry_data: dict[str, Any], + mqtt_config_entry_options: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=mqtt_config_entry_data, + options=mqtt_config_entry_options, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) @@ -1671,16 +1745,23 @@ async def test_default_will_message( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_WILL_MESSAGE: {}})], ) async def test_no_will_message( hass: HomeAssistant, mqtt_config_entry_data: dict[str, Any], + mqtt_config_entry_options: dict[str, Any], mqtt_client_mock: MqttMockPahoClient, ) -> None: """Test will message.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=mqtt_config_entry_data, + options=mqtt_config_entry_options, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) assert await hass.config_entries.async_setup(entry.entry_id) @@ -1690,7 +1771,7 @@ async def test_no_will_message( @pytest.mark.parametrize( - "mqtt_config_entry_data", + "mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], ) async def test_mqtt_subscribes_topics_on_connect( @@ -1709,12 +1790,12 @@ async def test_mqtt_subscribes_topics_on_connect( await mqtt.async_subscribe(hass, "still/pending", record_calls, 1) await mock_debouncer.wait() - mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_disconnect(Mock(), None, 0, MockMqttReasonCode()) mqtt_client_mock.reset_mock() mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) + mqtt_client_mock.on_connect(Mock(), None, 0, MockMqttReasonCode()) await mock_debouncer.wait() subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) @@ -1723,7 +1804,7 @@ async def test_mqtt_subscribes_topics_on_connect( assert ("still/pending", 1) in subscribe_calls -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@pytest.mark.parametrize("mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE]) async def test_mqtt_subscribes_wildcard_topics_in_correct_order( hass: HomeAssistant, mock_debouncer: asyncio.Event, @@ -1769,12 +1850,12 @@ async def test_mqtt_subscribes_wildcard_topics_in_correct_order( # Assert the initial wildcard topic subscription order _assert_subscription_order() - mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_disconnect(Mock(), None, 0, MockMqttReasonCode()) mqtt_client_mock.reset_mock() mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) + mqtt_client_mock.on_connect(Mock(), None, 0, MockMqttReasonCode()) await mock_debouncer.wait() # Assert the wildcard topic subscription order after a reconnect @@ -1782,7 +1863,7 @@ async def test_mqtt_subscribes_wildcard_topics_in_correct_order( @pytest.mark.parametrize( - "mqtt_config_entry_data", + "mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE | {mqtt.CONF_DISCOVERY: False}], ) async def test_mqtt_discovery_not_subscribes_when_disabled( @@ -1800,12 +1881,12 @@ async def test_mqtt_discovery_not_subscribes_when_disabled( assert (f"homeassistant/{component}/+/config", 0) not in subscribe_calls assert (f"homeassistant/{component}/+/+/config", 0) not in subscribe_calls - mqtt_client_mock.on_disconnect(Mock(), None, 0) + mqtt_client_mock.on_disconnect(Mock(), None, 0, MockMqttReasonCode()) mqtt_client_mock.reset_mock() mock_debouncer.clear() - mqtt_client_mock.on_connect(Mock(), None, 0, 0) + mqtt_client_mock.on_connect(Mock(), None, 0, MockMqttReasonCode()) await mock_debouncer.wait() subscribe_calls = help_all_subscribe_calls(mqtt_client_mock) @@ -1815,7 +1896,7 @@ async def test_mqtt_discovery_not_subscribes_when_disabled( @pytest.mark.parametrize( - "mqtt_config_entry_data", + "mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE], ) async def test_mqtt_subscribes_in_single_call( @@ -1841,7 +1922,7 @@ async def test_mqtt_subscribes_in_single_call( ] -@pytest.mark.parametrize("mqtt_config_entry_data", [ENTRY_DEFAULT_BIRTH_MESSAGE]) +@pytest.mark.parametrize("mqtt_config_entry_options", [ENTRY_DEFAULT_BIRTH_MESSAGE]) @patch("homeassistant.components.mqtt.client.MAX_SUBSCRIBES_PER_CALL", 2) @patch("homeassistant.components.mqtt.client.MAX_UNSUBSCRIBES_PER_CALL", 2) async def test_mqtt_subscribes_and_unsubscribes_in_chunks( @@ -1881,10 +1962,18 @@ async def test_mqtt_subscribes_and_unsubscribes_in_chunks( assert len(mqtt_client_mock.unsubscribe.mock_calls[1][1][0]) == 2 +@pytest.mark.parametrize( + "exception", + [ + OSError, + paho_mqtt.WebsocketConnectionError, + ], +) async def test_auto_reconnect( hass: HomeAssistant, setup_with_birth_msg_client_mock: MqttMockPahoClient, caplog: pytest.LogCaptureFixture, + exception: Exception, ) -> None: """Test reconnection is automatically done.""" mqtt_client_mock = setup_with_birth_msg_client_mock @@ -1892,10 +1981,10 @@ async def test_auto_reconnect( mqtt_client_mock.reconnect.reset_mock() mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() - mqtt_client_mock.reconnect.side_effect = OSError("foo") + mqtt_client_mock.reconnect.side_effect = exception("foo") async_fire_time_changed( hass, utcnow() + timedelta(seconds=RECONNECT_INTERVAL_SECONDS) ) @@ -1913,7 +2002,7 @@ async def test_auto_reconnect( hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) mqtt_client_mock.disconnect() - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() async_fire_time_changed( @@ -1955,7 +2044,7 @@ async def test_server_sock_connect_and_disconnect( mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_CONN_LOST mqtt_client_mock.on_socket_unregister_write(mqtt_client_mock, None, client) mqtt_client_mock.on_socket_close(mqtt_client_mock, None, client) - mqtt_client_mock.on_disconnect(mqtt_client_mock, None, client) + mqtt_client_mock.on_disconnect(mqtt_client_mock, None, None, MockMqttReasonCode()) await hass.async_block_till_done() mock_debouncer.clear() unsub() @@ -2006,7 +2095,7 @@ async def test_server_sock_buffer_size_with_websocket( client.setblocking(False) server.setblocking(False) - class FakeWebsocket(paho_mqtt.WebsocketWrapper): + class FakeWebsocket(paho_mqtt._WebsocketWrapper): def _do_handshake(self, *args, **kwargs): pass @@ -2093,4 +2182,4 @@ async def test_loop_write_failure( # Final for the disconnect callback await hass.async_block_till_done() - assert "Disconnected from MQTT server test-broker:1883" in caplog.text + assert "Error returned from MQTT server: The connection was lost." in caplog.text diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 95a26daf562..a34907adbaf 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -68,7 +68,7 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { _SENTINEL = object() -DISCOVERY_COUNT = len(MQTT) +DISCOVERY_COUNT = sum(len(discovery_topic) for discovery_topic in MQTT.values()) DEVICE_DISCOVERY_COUNT = 2 type _MqttMessageType = list[tuple[str, str]] @@ -1887,7 +1887,12 @@ async def help_test_reloadable( mqtt.DOMAIN: {domain: [old_config_1, old_config_2]}, } # Start the MQTT entry with the old config - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) mqtt_client_mock.connect.return_value = 0 with patch("homeassistant.config.load_yaml_config_file", return_value=old_config): diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 38dbda50cdd..de70fd32763 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -28,7 +28,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, MockMqttReasonCode from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient ADD_ON_DISCOVERY_INFO = { @@ -43,6 +43,28 @@ ADD_ON_DISCOVERY_INFO = { MOCK_CLIENT_CERT = b"## mock client certificate file ##" MOCK_CLIENT_KEY = b"## mock key file ##" +MOCK_ENTRY_DATA = { + mqtt.CONF_BROKER: "test-broker", + CONF_PORT: 1234, + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", +} +MOCK_ENTRY_OPTIONS = { + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/online", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 1, + mqtt.ATTR_RETAIN: True, + }, + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/offline", + mqtt.ATTR_PAYLOAD: "offline", + mqtt.ATTR_QOS: 2, + mqtt.ATTR_RETAIN: False, + }, +} + @pytest.fixture(autouse=True) def mock_finish_setup() -> Generator[MagicMock]: @@ -121,16 +143,16 @@ def mock_try_connection_success() -> Generator[MqttMockPahoClient]: def loop_start(): """Simulate connect on loop start.""" - mock_client().on_connect(mock_client, None, None, 0) + mock_client().on_connect(mock_client, None, None, MockMqttReasonCode(), None) def _subscribe(topic, qos=0): mid = get_mid() - mock_client().on_subscribe(mock_client, 0, mid) + mock_client().on_subscribe(mock_client, 0, mid, [MockMqttReasonCode()], None) return (0, mid) def _unsubscribe(topic): mid = get_mid() - mock_client().on_unsubscribe(mock_client, 0, mid) + mock_client().on_unsubscribe(mock_client, 0, mid, [MockMqttReasonCode()], None) return (0, mid) with patch( @@ -243,8 +265,10 @@ async def test_user_connection_works( assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, - "discovery": True, } + # Check we have the latest Config Entry version + assert result["result"].version == 1 + assert result["result"].minor_version == 2 # Check we tried the connection assert len(mock_try_connection.mock_calls) == 1 # Check config entry got setup @@ -283,7 +307,6 @@ async def test_user_connection_works_with_supervisor( assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, - "discovery": True, } # Check we tried the connection assert len(mock_try_connection.mock_calls) == 1 @@ -324,7 +347,6 @@ async def test_user_v5_connection_works( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].data == { "broker": "another-broker", - "discovery": True, "port": 2345, "protocol": "5", } @@ -383,14 +405,12 @@ async def test_manual_config_set( assert result["result"].data == { "broker": "127.0.0.1", "port": 1883, - "discovery": True, } # Check we tried the connection, with precedence for config entry settings mock_try_connection.assert_called_once_with( { "broker": "127.0.0.1", "port": 1883, - "discovery": True, }, ) # Check config entry got setup @@ -401,7 +421,11 @@ async def test_manual_config_set( async def test_user_single_instance(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" - MockConfigEntry(domain="mqtt").add_to_hass(hass) + MockConfigEntry( + domain="mqtt", + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_USER} @@ -412,7 +436,11 @@ async def test_user_single_instance(hass: HomeAssistant) -> None: async def test_hassio_already_configured(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" - MockConfigEntry(domain="mqtt").add_to_hass(hass) + MockConfigEntry( + domain="mqtt", + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( "mqtt", context={"source": config_entries.SOURCE_HASSIO} @@ -424,7 +452,10 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: async def test_hassio_ignored(hass: HomeAssistant) -> None: """Test we supervisor discovered instance can be ignored.""" MockConfigEntry( - domain=mqtt.DOMAIN, source=config_entries.SOURCE_IGNORE + domain=mqtt.DOMAIN, + source=config_entries.SOURCE_IGNORE, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -934,43 +965,19 @@ async def test_addon_not_installed_failures( async def test_option_flow( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - mock_try_connection: MagicMock, ) -> None: """Test config flow options.""" with patch( "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value={}) ) as yaml_mock: - mqtt_mock = await mqtt_mock_entry() - mock_try_connection.return_value = True + await mqtt_mock_entry() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - hass.config_entries.async_update_entry( - config_entry, - data={ - mqtt.CONF_BROKER: "test-broker", - CONF_PORT: 1234, - }, - ) - - mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "broker" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - }, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() - assert mqtt_mock.async_connect.call_count == 0 yaml_mock.reset_mock() @@ -992,12 +999,10 @@ async def test_option_flow( }, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert config_entry.data == { - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", + await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + assert config_entry.data == {mqtt.CONF_BROKER: "mock-broker"} + assert config_entry.options == { mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_BIRTH_MESSAGE: { @@ -1015,8 +1020,7 @@ async def test_option_flow( } await hass.async_block_till_done() - assert config_entry.title == "another-broker" - # assert that the entry was reloaded with the new config + # assert that the entry was reloaded with the new config assert yaml_mock.await_count @@ -1071,7 +1075,7 @@ async def test_bad_certificate( test_input.pop(mqtt.CONF_CLIENT_KEY) mqtt_mock = await mqtt_mock_entry() - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + config_entry: MockConfigEntry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] # Add at least one advanced option to get the full form hass.config_entries.async_update_entry( config_entry, @@ -1088,11 +1092,11 @@ async def test_bad_certificate( mqtt_mock.async_connect.reset_mock() - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await config_entry.start_reconfigure_flow(hass, show_advanced_options=True) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", @@ -1109,14 +1113,14 @@ async def test_bad_certificate( test_input["set_ca_cert"] = set_ca_cert test_input["tls_insecure"] = tls_insecure - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=test_input, ) if test_error is not None: assert result["errors"]["base"] == test_error return - assert result["errors"] == {} + assert "errors" not in result @pytest.mark.parametrize( @@ -1148,7 +1152,7 @@ async def test_keepalive_validation( mqtt_mock = await mqtt_mock_entry() mock_try_connection.return_value = True - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + config_entry: MockConfigEntry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] # Add at least one advanced option to get the full form hass.config_entries.async_update_entry( config_entry, @@ -1161,22 +1165,23 @@ async def test_keepalive_validation( mqtt_mock.async_connect.reset_mock() - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await config_entry.start_reconfigure_flow(hass, show_advanced_options=True) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" if error: with pytest.raises(vol.Invalid): - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=test_input, ) return - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=test_input, ) - assert not result["errors"] + assert "errors" not in result + assert result["reason"] == "reconfigure_successful" async def test_disable_birth_will( @@ -1186,7 +1191,7 @@ async def test_disable_birth_will( mock_reload_after_entry_update: MagicMock, ) -> None: """Test disabling birth and will.""" - mqtt_mock = await mqtt_mock_entry() + await mqtt_mock_entry() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] hass.config_entries.async_update_entry( @@ -1199,26 +1204,10 @@ async def test_disable_birth_will( await hass.async_block_till_done() mock_reload_after_entry_update.reset_mock() - mqtt_mock.async_connect.reset_mock() - result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "broker" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - }, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" - await hass.async_block_till_done() - assert mqtt_mock.async_connect.call_count == 0 result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -1238,12 +1227,14 @@ async def test_disable_birth_will( }, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert config_entry.data == { - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", + assert result["data"] == { + "birth_message": {}, + "discovery": True, + "discovery_prefix": "homeassistant", + "will_message": {}, + } + assert config_entry.data == {mqtt.CONF_BROKER: "test-broker", CONF_PORT: 1234} + assert config_entry.options == { mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", mqtt.CONF_BIRTH_MESSAGE: {}, @@ -1270,6 +1261,8 @@ async def test_invalid_discovery_prefix( data={ mqtt.CONF_BROKER: "test-broker", CONF_PORT: 1234, + }, + options={ mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", }, @@ -1280,16 +1273,6 @@ async def test_invalid_discovery_prefix( result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "broker" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - }, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -1308,6 +1291,8 @@ async def test_invalid_discovery_prefix( assert config_entry.data == { mqtt.CONF_BROKER: "test-broker", CONF_PORT: 1234, + } + assert config_entry.options == { mqtt.CONF_DISCOVERY: True, mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", } @@ -1356,6 +1341,8 @@ async def test_option_flow_default_suggested_values( CONF_PORT: 1234, CONF_USERNAME: "user", CONF_PASSWORD: "pass", + }, + options={ mqtt.CONF_DISCOVERY: True, mqtt.CONF_BIRTH_MESSAGE: { mqtt.ATTR_TOPIC: "ha_state/online", @@ -1371,37 +1358,13 @@ async def test_option_flow_default_suggested_values( }, }, ) + await hass.async_block_till_done() # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "broker" - defaults = { - mqtt.CONF_BROKER: "test-broker", - CONF_PORT: 1234, - } - suggested = { - CONF_USERNAME: "user", - CONF_PASSWORD: PWD_NOT_CHANGED, - } - for key, value in defaults.items(): - assert get_default(result["data_schema"].schema, key) == value - for key, value in suggested.items(): - assert get_suggested(result["data_schema"].schema, key) == value - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - CONF_USERNAME: "us3r", - CONF_PASSWORD: "p4ss", - }, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" defaults = { - mqtt.CONF_DISCOVERY: True, "birth_qos": 1, "birth_retain": True, "will_qos": 2, @@ -1421,7 +1384,6 @@ async def test_option_flow_default_suggested_values( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - mqtt.CONF_DISCOVERY: False, "birth_topic": "ha_state/onl1ne", "birth_payload": "onl1ne", "birth_qos": 2, @@ -1437,28 +1399,8 @@ async def test_option_flow_default_suggested_values( # Test updated default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "broker" - defaults = { - mqtt.CONF_BROKER: "another-broker", - CONF_PORT: 2345, - } - suggested = { - CONF_USERNAME: "us3r", - CONF_PASSWORD: PWD_NOT_CHANGED, - } - for key, value in defaults.items(): - assert get_default(result["data_schema"].schema, key) == value - for key, value in suggested.items(): - assert get_suggested(result["data_schema"].schema, key) == value - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345}, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" defaults = { - mqtt.CONF_DISCOVERY: False, "birth_qos": 2, "birth_retain": False, "will_qos": 1, @@ -1478,7 +1420,6 @@ async def test_option_flow_default_suggested_values( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - mqtt.CONF_DISCOVERY: True, "birth_topic": "ha_state/onl1ne", "birth_payload": "onl1ne", "birth_qos": 2, @@ -1496,7 +1437,8 @@ async def test_option_flow_default_suggested_values( @pytest.mark.parametrize( - ("advanced_options", "step_id"), [(False, "options"), (True, "broker")] + ("advanced_options", "flow_result"), + [(False, FlowResultType.ABORT), (True, FlowResultType.FORM)], ) @pytest.mark.usefixtures("mock_reload_after_entry_update") async def test_skipping_advanced_options( @@ -1504,41 +1446,35 @@ async def test_skipping_advanced_options( mqtt_mock_entry: MqttMockHAClientGenerator, mock_try_connection: MagicMock, advanced_options: bool, - step_id: str, + flow_result: FlowResultType, ) -> None: """Test advanced options option.""" test_input = { mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345, - "advanced_options": advanced_options, } + if advanced_options: + test_input["advanced_options"] = True mqtt_mock = await mqtt_mock_entry() mock_try_connection.return_value = True - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - # Initiate with a basic setup - hass.config_entries.async_update_entry( - config_entry, - data={ - mqtt.CONF_BROKER: "test-broker", - CONF_PORT: 1234, - }, - ) - + config_entry: MockConfigEntry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_mock.async_connect.reset_mock() - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context={"show_advanced_options": True} + result = await config_entry.start_reconfigure_flow( + hass, show_advanced_options=advanced_options ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" - result = await hass.config_entries.options.async_configure( + assert ("advanced_options" in result["data_schema"].schema) == advanced_options + + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=test_input, ) - assert result["step_id"] == step_id + assert result["type"] is flow_result @pytest.mark.parametrize( @@ -1582,7 +1518,12 @@ async def test_step_reauth( """Test that the reauth step works.""" # Prepare the config entry - config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=test_input) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=test_input, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -1658,7 +1599,12 @@ async def test_step_hassio_reauth( addon_info["hostname"] = "core-mosquitto" # Prepare the config entry - config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=entry_data) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=entry_data, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -1740,7 +1686,12 @@ async def test_step_hassio_reauth_no_discovery_info( addon_info["hostname"] = "core-mosquitto" # Prepare the config entry - config_entry = MockConfigEntry(domain=mqtt.DOMAIN, data=entry_data) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data=entry_data, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -1762,11 +1713,15 @@ async def test_step_hassio_reauth_no_discovery_info( mock_try_connection.assert_not_called() -async def test_options_user_connection_fails( +async def test_reconfigure_user_connection_fails( hass: HomeAssistant, mock_try_connection_time_out: MagicMock ) -> None: """Test if connection cannot be made.""" - config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( config_entry, @@ -1775,11 +1730,11 @@ async def test_options_user_connection_fails( CONF_PORT: 1234, }, ) - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await config_entry.start_reconfigure_flow(hass, show_advanced_options=True) assert result["type"] is FlowResultType.FORM mock_try_connection_time_out.reset_mock() - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={mqtt.CONF_BROKER: "bad-broker", CONF_PORT: 2345}, ) @@ -1800,7 +1755,11 @@ async def test_options_bad_birth_message_fails( hass: HomeAssistant, mock_try_connection: MqttMockPahoClient ) -> None: """Test bad birth message.""" - config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( config_entry, @@ -1813,13 +1772,6 @@ async def test_options_bad_birth_message_fails( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] is FlowResultType.FORM - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345}, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" @@ -1841,7 +1793,11 @@ async def test_options_bad_will_message_fails( hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: """Test bad will message.""" - config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( config_entry, @@ -1854,13 +1810,6 @@ async def test_options_bad_will_message_fails( mock_try_connection.return_value = True result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] is FlowResultType.FORM - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={mqtt.CONF_BROKER: "another-broker", CONF_PORT: 2345}, - ) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "options" @@ -1878,15 +1827,16 @@ async def test_options_bad_will_message_fails( } -@pytest.mark.parametrize( - "hass_config", [{"mqtt": {"sensor": [{"state_topic": "some-topic"}]}}] -) @pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") async def test_try_connection_with_advanced_parameters( hass: HomeAssistant, mock_try_connection_success: MqttMockPahoClient ) -> None: """Test config flow with advanced parameters from config.""" - config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( config_entry, @@ -1920,7 +1870,7 @@ async def test_try_connection_with_advanced_parameters( ) # Test default/suggested values from config - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await config_entry.start_reconfigure_flow(hass, show_advanced_options=True) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" defaults = { @@ -1944,9 +1894,8 @@ async def test_try_connection_with_advanced_parameters( assert get_suggested(result["data_schema"].schema, k) == v # test we can change username and password - # as it was configured as auto in configuration.yaml is is migrated now mock_try_connection_success.reset_mock() - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ mqtt.CONF_BROKER: "another-broker", @@ -1961,9 +1910,8 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_WS_HEADERS: '{"h3": "v3"}', }, ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} - assert result["step_id"] == "options" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" await hass.async_block_till_done() # check if the username and password was set from config flow and not from configuration.yaml @@ -1987,12 +1935,6 @@ async def test_try_connection_with_advanced_parameters( "/new/path", {"h3": "v3"}, ) - # Accept default option - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={}, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -2005,7 +1947,11 @@ async def test_setup_with_advanced_settings( """Test config flow setup with advanced parameters.""" file_id = mock_process_uploaded_file.file_id - config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( config_entry, @@ -2017,15 +1963,13 @@ async def test_setup_with_advanced_settings( mock_try_connection.return_value = True - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context={"show_advanced_options": True} - ) + result = await config_entry.start_reconfigure_flow(hass, show_advanced_options=True) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "broker" assert result["data_schema"].schema["advanced_options"] # first iteration, basic settings - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", @@ -2049,7 +1993,7 @@ async def test_setup_with_advanced_settings( assert mqtt.CONF_CLIENT_KEY not in result["data_schema"].schema # second iteration, advanced settings with request for client cert - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", @@ -2080,7 +2024,7 @@ async def test_setup_with_advanced_settings( assert result["data_schema"].schema[mqtt.CONF_WS_HEADERS] # third iteration, advanced settings with client cert and key set and bad json payload - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", @@ -2105,7 +2049,7 @@ async def test_setup_with_advanced_settings( # fourth iteration, advanced settings with client cert and key set # and correct json payload for ws_headers - result = await hass.config_entries.options.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ mqtt.CONF_BROKER: "test-broker", @@ -2124,17 +2068,8 @@ async def test_setup_with_advanced_settings( }, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "options" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - mqtt.CONF_DISCOVERY: True, - mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", - }, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" # Check config entry result assert config_entry.data == { @@ -2153,8 +2088,6 @@ async def test_setup_with_advanced_settings( "header_2": "content_header_2", }, mqtt.CONF_CERTIFICATE: "auto", - mqtt.CONF_DISCOVERY: True, - mqtt.CONF_DISCOVERY_PREFIX: "homeassistant_test", } @@ -2163,7 +2096,11 @@ async def test_change_websockets_transport_to_tcp( hass: HomeAssistant, mock_try_connection: MagicMock ) -> None: """Test reconfiguration flow changing websockets transport settings.""" - config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( config_entry, @@ -2254,3 +2191,150 @@ async def test_reconfigure_flow_form( mqtt.CONF_WS_PATH: "/some_new_path", } await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.mark.usefixtures("mock_ssl_context", "mock_process_uploaded_file") +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "test-broker", + CONF_USERNAME: "mqtt-user", + CONF_PASSWORD: "mqtt-password", + CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, + mqtt.CONF_WS_PATH: "/some_path", + } + ], +) +async def test_reconfigure_no_changed_password( + hass: HomeAssistant, + mock_try_connection: MagicMock, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test reconfigure flow.""" + await mqtt_mock_entry() + entry: MockConfigEntry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + result = await entry.start_reconfigure_flow(hass, show_advanced_options=True) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "broker" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "10.10.10,10", + CONF_USERNAME: "mqtt-user", + CONF_PASSWORD: PWD_NOT_CHANGED, + CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: '{"header_1": "custom_header1"}', + mqtt.CONF_WS_PATH: "/some_new_path", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + mqtt.CONF_BROKER: "10.10.10,10", + CONF_USERNAME: "mqtt-user", + CONF_PASSWORD: "mqtt-password", + CONF_PORT: 1234, + mqtt.CONF_TRANSPORT: "websockets", + mqtt.CONF_WS_HEADERS: {"header_1": "custom_header1"}, + mqtt.CONF_WS_PATH: "/some_new_path", + } + await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.mark.parametrize( + ( + "version", + "minor_version", + "data", + "options", + "expected_version", + "expected_minor_version", + ), + [ + (1, 1, MOCK_ENTRY_DATA | MOCK_ENTRY_OPTIONS, {}, 1, 2), + (1, 2, MOCK_ENTRY_DATA, MOCK_ENTRY_OPTIONS, 1, 2), + (1, 3, MOCK_ENTRY_DATA, MOCK_ENTRY_OPTIONS, 1, 3), + ], +) +@pytest.mark.usefixtures("mock_reload_after_entry_update") +async def test_migrate_config_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + version: int, + minor_version: int, + data: dict[str, Any], + options: dict[str, Any], + expected_version: int, + expected_minor_version: int, +) -> None: + """Test migrating a config entry.""" + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + # Mock to a migratable or compatbible config entry version + hass.config_entries.async_update_entry( + config_entry, + data=data, + options=options, + version=version, + minor_version=minor_version, + ) + await hass.async_block_till_done() + # Start MQTT + await mqtt_mock_entry() + await hass.async_block_till_done() + assert ( + config_entry.data | config_entry.options == MOCK_ENTRY_DATA | MOCK_ENTRY_OPTIONS + ) + assert config_entry.version == expected_version + assert config_entry.minor_version == expected_minor_version + + +@pytest.mark.parametrize( + ( + "version", + "minor_version", + "data", + "options", + "expected_version", + "expected_minor_version", + ), + [ + (2, 1, MOCK_ENTRY_DATA, MOCK_ENTRY_OPTIONS, 2, 1), + ], +) +@pytest.mark.usefixtures("mock_reload_after_entry_update") +async def test_migrate_of_incompatible_config_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + version: int, + minor_version: int, + data: dict[str, Any], + options: dict[str, Any], + expected_version: int, + expected_minor_version: int, +) -> None: + """Test migrating a config entry.""" + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + # Mock an incompatible config entry version + hass.config_entries.async_update_entry( + config_entry, + data=data, + options=options, + version=version, + minor_version=minor_version, + ) + await hass.async_block_till_done() + assert config_entry.version == expected_version + assert config_entry.minor_version == expected_minor_version + + # Try to start MQTT with incompatible config entry + with pytest.raises(AssertionError): + await mqtt_mock_entry() + + assert config_entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index b8499ba5812..bbd60329d0a 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -17,10 +17,12 @@ from tests.components.diagnostics import ( ) from tests.typing import ClientSessionGenerator, MqttMockHAClientGenerator -default_config = { - "birth_message": {}, +default_entry_data = { "broker": "mock-broker", } +default_entry_options = { + "birth_message": {}, +} async def test_entry_diagnostics( @@ -38,7 +40,7 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "connected": True, "devices": [], - "mqtt_config": default_config, + "mqtt_config": {"data": default_entry_data, "options": default_entry_options}, "mqtt_debug_info": {"entities": [], "triggers": []}, } @@ -123,7 +125,7 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "connected": True, "devices": [expected_device], - "mqtt_config": default_config, + "mqtt_config": {"data": default_entry_data, "options": default_entry_options}, "mqtt_debug_info": expected_debug_info, } @@ -132,20 +134,24 @@ async def test_entry_diagnostics( ) == { "connected": True, "device": expected_device, - "mqtt_config": default_config, + "mqtt_config": {"data": default_entry_data, "options": default_entry_options}, "mqtt_debug_info": expected_debug_info, } @pytest.mark.parametrize( - "mqtt_config_entry_data", + ("mqtt_config_entry_data", "mqtt_config_entry_options"), [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - CONF_PASSWORD: "hunter2", - CONF_USERNAME: "my_user", - } + ( + { + mqtt.CONF_BROKER: "mock-broker", + CONF_PASSWORD: "hunter2", + CONF_USERNAME: "my_user", + }, + { + mqtt.CONF_BIRTH_MESSAGE: {}, + }, + ) ], ) async def test_redact_diagnostics( @@ -157,9 +163,12 @@ async def test_redact_diagnostics( ) -> None: """Test redacting diagnostics.""" mqtt_mock = await mqtt_mock_entry() - expected_config = dict(default_config) - expected_config["password"] = "**REDACTED**" - expected_config["username"] = "**REDACTED**" + expected_config = { + "data": dict(default_entry_data), + "options": dict(default_entry_options), + } + expected_config["data"]["password"] = "**REDACTED**" + expected_config["data"]["username"] = "**REDACTED**" config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_mock.connected = True diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 8a674a4e1cd..982167feee1 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -195,8 +195,8 @@ async def mock_mqtt_flow( @pytest.mark.parametrize( - "mqtt_config_entry_data", - [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], + ("mqtt_config_entry_data", "mqtt_config_entry_options"), + [({mqtt.CONF_BROKER: "mock-broker"}, {mqtt.CONF_DISCOVERY: False})], ) async def test_subscribing_config_topic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator @@ -356,7 +356,7 @@ async def test_invalid_device_discovery_config( async_fire_mqtt_message( hass, "homeassistant/device/bla/config", - '{ "o": {"name": "foobar"}, "dev": {"identifiers": ["ABDE03"]}, ' '"cmps": ""}', + '{ "o": {"name": "foobar"}, "dev": {"identifiers": ["ABDE03"]}, "cmps": ""}', ) await hass.async_block_till_done() assert ( @@ -1946,7 +1946,12 @@ async def test_cleanup_device_multiple_config_entries( mqtt_mock = await mqtt_mock_entry() ws_client = await hass_ws_client(hass) - config_entry = MockConfigEntry(domain="test", data={}) + config_entry = MockConfigEntry( + domain="test", + data={}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -2042,7 +2047,12 @@ async def test_cleanup_device_multiple_config_entries_mqtt( ) -> None: """Test discovered device is cleaned up when removed through MQTT.""" mqtt_mock = await mqtt_mock_entry() - config_entry = MockConfigEntry(domain="test", data={}) + config_entry = MockConfigEntry( + domain="test", + data={}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -2437,12 +2447,14 @@ async def test_no_implicit_state_topic_switch( @pytest.mark.parametrize( - "mqtt_config_entry_data", + ("mqtt_config_entry_data", "mqtt_config_entry_options"), [ - { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", - } + ( + {mqtt.CONF_BROKER: "mock-broker"}, + { + mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", + }, + ) ], ) async def test_complex_discovery_topic_prefix( @@ -2497,7 +2509,13 @@ async def test_mqtt_integration_discovery_flow_fitering_on_redundant_payload( """Handle birth message.""" birth.set() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=ENTRY_DEFAULT_BIRTH_MESSAGE) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "mock-broker"}, + options=ENTRY_DEFAULT_BIRTH_MESSAGE, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) with ( patch( @@ -2562,7 +2580,13 @@ async def test_mqtt_discovery_flow_starts_once( """Handle birth message.""" birth.set() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=ENTRY_DEFAULT_BIRTH_MESSAGE) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "mock-broker"}, + options=ENTRY_DEFAULT_BIRTH_MESSAGE, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) with ( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 2ab664f5041..af9975de1ea 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -13,6 +13,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol +from homeassistant import core as ha from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.models import ( @@ -30,7 +31,6 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -import homeassistant.core as ha from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er, template @@ -44,6 +44,8 @@ from homeassistant.util.dt import utcnow from tests.common import ( MockConfigEntry, MockEntity, + MockEntityPlatform, + MockMqttReasonCode, async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, @@ -194,6 +196,7 @@ async def test_value_template_value(hass: HomeAssistant) -> None: # test value template with entity entity = Entity() entity.hass = hass + entity.platform = MockEntityPlatform(hass) entity.entity_id = "select.test" tpl = template.Template("{{ value_json.id }}", hass=hass) val_tpl = mqtt.MqttValueTemplate(tpl, entity=entity) @@ -218,6 +221,7 @@ async def test_value_template_fails(hass: HomeAssistant) -> None: """Test the rendering of MQTT value template fails.""" entity = MockEntity(entity_id="sensor.test") entity.hass = hass + entity.platform = MockEntityPlatform(hass) tpl = template.Template("{{ value_json.some_var * 2 }}", hass=hass) val_tpl = mqtt.MqttValueTemplate(tpl, entity=entity) with pytest.raises(MqttValueTemplateException) as exc: @@ -275,80 +279,6 @@ async def test_service_call_mqtt_entry_does_not_publish( ) -# The use of a topic_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_mqtt_publish_action_call_with_topic_and_topic_template_does_not_publish( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the mqtt publish action call with topic/topic template. - - If both 'topic' and 'topic_template' are provided then fail. - """ - mqtt_mock = await mqtt_mock_entry() - topic = "test/topic" - topic_template = "test/{{ 'topic' }}" - with pytest.raises(vol.Invalid): - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC: topic, - mqtt.ATTR_TOPIC_TEMPLATE: topic_template, - mqtt.ATTR_PAYLOAD: "payload", - }, - blocking=True, - ) - assert not mqtt_mock.async_publish.called - - -# The use of a topic_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_mqtt_action_call_with_invalid_topic_template_does_not_publish( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the mqtt publish action call with a problematic topic template.""" - mqtt_mock = await mqtt_mock_entry() - with pytest.raises(MqttCommandTemplateException) as exc: - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ 1 | no_such_filter }}", - mqtt.ATTR_PAYLOAD: "payload", - }, - blocking=True, - ) - assert str(exc.value) == ( - "TemplateError: TemplateAssertionError: No filter named 'no_such_filter'. " - "rendering template, template: " - "'test/{{ 1 | no_such_filter }}' and payload: None" - ) - assert not mqtt_mock.async_publish.called - - -# The use of a topic_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_mqtt_publish_action_call_with_template_topic_renders_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the mqtt publish action call with rendered topic template. - - If 'topic_template' is provided and 'topic' is not, then render it. - """ - mqtt_mock = await mqtt_mock_entry() - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ 1+1 }}", - mqtt.ATTR_PAYLOAD: "payload", - }, - blocking=True, - ) - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][0] == "test/2" - - async def test_service_call_with_template_topic_renders_invalid_topic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: @@ -357,84 +287,23 @@ async def test_service_call_with_template_topic_renders_invalid_topic( If a wildcard topic is rendered, then fail. """ mqtt_mock = await mqtt_mock_entry() - with pytest.raises(ServiceValidationError) as exc: + with pytest.raises(vol.Invalid) as exc: await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, { - mqtt.ATTR_TOPIC_TEMPLATE: "test/{{ '+' if True else 'topic' }}/topic", + mqtt.ATTR_TOPIC: "test/{{ '+' if True else 'topic' }}/topic", mqtt.ATTR_PAYLOAD: "payload", }, blocking=True, ) - assert str(exc.value) == ( - "Unable to publish: topic template `test/{{ '+' if True else 'topic' }}/topic` " - "produced an invalid topic `test/+/topic` after rendering " - "(Wildcards cannot be used in topic names)" + assert ( + str(exc.value) == "Wildcards cannot be used in topic names " + "for dictionary value @ data['topic']" ) assert not mqtt_mock.async_publish.called -# The use of a payload_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_action_call_with_invalid_rendered_payload_template_doesnt_render_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the action call with unrendered payload template. - - If both 'payload' and 'payload_template' are provided then fail. - """ - mqtt_mock = await mqtt_mock_entry() - payload = "not a template" - payload_template = "a template" - with pytest.raises(vol.Invalid): - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC: "test/topic", - mqtt.ATTR_PAYLOAD: payload, - mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template, - }, - blocking=True, - ) - assert not mqtt_mock.async_publish.called - - -# The use of a payload_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_mqtt_publish_action_call_with_template_payload_renders_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the mqtt publish action call with rendered template. - - If 'payload_template' is provided and 'payload' is not, then render it. - """ - mqtt_mock = await mqtt_mock_entry() - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - {mqtt.ATTR_TOPIC: "test/topic", mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ 4+4 }}"}, - blocking=True, - ) - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][1] == "8" - mqtt_mock.reset_mock() - - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC: "test/topic", - mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ (4+4) | pack('B') }}", - }, - blocking=True, - ) - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][1] == b"\x08" - mqtt_mock.reset_mock() - - @pytest.mark.parametrize( ("attr_payload", "payload", "evaluate_payload", "literal_eval_calls"), [ @@ -503,56 +372,6 @@ async def test_mqtt_publish_action_call_with_raw_data( assert len(literal_eval_mock.mock_calls) == literal_eval_calls -# The use of a payload_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_publish_action_call_with_bad_payload_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the mqtt publish action call with a bad template does not publish.""" - mqtt_mock = await mqtt_mock_entry() - with pytest.raises(MqttCommandTemplateException) as exc: - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC: "test/topic", - mqtt.ATTR_PAYLOAD_TEMPLATE: "{{ 1 | bad }}", - }, - blocking=True, - ) - assert not mqtt_mock.async_publish.called - assert str(exc.value) == ( - "TemplateError: TemplateAssertionError: No filter named 'bad'. " - "rendering template, template: '{{ 1 | bad }}' and payload: None" - ) - - -# The use of a payload_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_action_call_with_payload_doesnt_render_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the mqtt publish action call with an unrendered template. - - If both 'payload' and 'payload_template' are provided then fail. - """ - mqtt_mock = await mqtt_mock_entry() - payload = "not a template" - payload_template = "a template" - with pytest.raises(vol.Invalid): - await hass.services.async_call( - mqtt.DOMAIN, - mqtt.SERVICE_PUBLISH, - { - mqtt.ATTR_TOPIC: "test/topic", - mqtt.ATTR_PAYLOAD: payload, - mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template, - }, - blocking=True, - ) - assert not mqtt_mock.async_publish.called - - async def test_service_call_with_ascii_qos_retain_flags( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: @@ -573,6 +392,25 @@ async def test_service_call_with_ascii_qos_retain_flags( blocking=True, ) assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][1] == "" + assert mqtt_mock.async_publish.call_args[0][2] == 2 + assert not mqtt_mock.async_publish.call_args[0][3] + + mqtt_mock.reset_mock() + + # Test service call without payload + await hass.services.async_call( + mqtt.DOMAIN, + mqtt.SERVICE_PUBLISH, + { + mqtt.ATTR_TOPIC: "test/topic", + mqtt.ATTR_QOS: "2", + mqtt.ATTR_RETAIN: "no", + }, + blocking=True, + ) + assert mqtt_mock.async_publish.called + assert mqtt_mock.async_publish.call_args[0][1] is None assert mqtt_mock.async_publish.call_args[0][2] == 2 assert not mqtt_mock.async_publish.call_args[0][3] @@ -877,7 +715,12 @@ async def test_reload_entry_with_restored_subscriptions( ) -> None: """Test reloading the config entry with with subscriptions restored.""" # Setup the MQTT entry - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) with patch("homeassistant.config.load_yaml_config_file", return_value={}): @@ -982,7 +825,10 @@ async def test_default_entry_setting_are_applied( # Config entry data is incomplete but valid according the schema entry = MockConfigEntry( - domain=mqtt.DOMAIN, data={"broker": "test-broker", "port": 1234} + domain=mqtt.DOMAIN, + data={"broker": "test-broker", "port": 1234}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) hass.config.components.add(mqtt.DOMAIN) @@ -1721,65 +1567,13 @@ async def test_debug_info_qos_retain( } in messages -# The use of a payload_template in an mqtt publish action call -# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0 -async def test_publish_json_from_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the publishing of call to mqtt publish action.""" - mqtt_mock = await mqtt_mock_entry() - - test_str = "{'valid': 'python', 'invalid': 'json'}" - test_str_tpl = "{'valid': '{{ \"python\" }}', 'invalid': 'json'}" - - await async_setup_component( - hass, - "script", - { - "script": { - "test_script_payload": { - "sequence": { - "service": "mqtt.publish", - "data": {"topic": "test-topic", "payload": test_str_tpl}, - } - }, - "test_script_payload_template": { - "sequence": { - "service": "mqtt.publish", - "data": { - "topic": "test-topic", - "payload_template": test_str_tpl, - }, - } - }, - } - }, - ) - - await hass.services.async_call("script", "test_script_payload", blocking=True) - await hass.async_block_till_done() - - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][1] == test_str - - mqtt_mock.async_publish.reset_mock() - assert not mqtt_mock.async_publish.called - - await hass.services.async_call( - "script", "test_script_payload_template", blocking=True - ) - await hass.async_block_till_done() - - assert mqtt_mock.async_publish.called - assert mqtt_mock.async_publish.call_args[0][1] == test_str - - async def test_subscribe_connection_status( hass: HomeAssistant, mock_debouncer: asyncio.Event, setup_with_birth_msg_client_mock: MqttMockPahoClient, ) -> None: """Test connextion status subscription.""" + mqtt_client_mock = setup_with_birth_msg_client_mock mqtt_connected_calls_callback: list[bool] = [] mqtt_connected_calls_async: list[bool] = [] @@ -1797,7 +1591,7 @@ async def test_subscribe_connection_status( assert mqtt.is_connected(hass) is True # Mock disconnect status - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() assert mqtt.is_connected(hass) is False @@ -1811,12 +1605,12 @@ async def test_subscribe_connection_status( # Mock connect status mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, MockMqttReasonCode()) await mock_debouncer.wait() assert mqtt.is_connected(hass) is True # Mock disconnect status - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() assert mqtt.is_connected(hass) is False @@ -1826,7 +1620,7 @@ async def test_subscribe_connection_status( # Mock connect status mock_debouncer.clear() - mqtt_client_mock.on_connect(None, None, 0, 0) + mqtt_client_mock.on_connect(None, None, 0, MockMqttReasonCode()) await mock_debouncer.wait() assert mqtt.is_connected(hass) is True @@ -1849,6 +1643,8 @@ async def test_unload_config_entry( entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index dbca09e803c..f8c66a3de1d 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -72,7 +72,7 @@ mqtt: payload_on: "on" payload_off: "off" -config with brightness and color temp +config with brightness and color temp (mired) mqtt: light: @@ -88,6 +88,23 @@ mqtt: payload_on: "on" payload_off: "off" +config with brightness and color temp (Kelvin) + +mqtt: + light: + - name: "Office Light Color Temp" + state_topic: "office/rgb1/light/status" + command_topic: "office/rgb1/light/switch" + brightness_state_topic: "office/rgb1/brightness/status" + brightness_command_topic: "office/rgb1/brightness/set" + brightness_scale: 99 + color_temp_kelvin: true + color_temp_state_topic: "office/rgb1/color_temp/status" + color_temp_command_topic: "office/rgb1/color_temp/set" + qos: 0 + payload_on: "on" + payload_off: "off" + config with brightness and effect mqtt: @@ -305,6 +322,101 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.state == STATE_UNKNOWN +@pytest.mark.parametrize( + ("hass_config", "min_kelvin", "max_kelvin"), + [ + ( + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + }, + ), + ), + light.DEFAULT_MIN_KELVIN, + light.DEFAULT_MAX_KELVIN, + ), + ( + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "min_mireds": 180, + }, + ), + ), + light.DEFAULT_MIN_KELVIN, + 5555, + ), + ( + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "max_mireds": 400, + }, + ), + ), + 2500, + light.DEFAULT_MAX_KELVIN, + ), + ( + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "max_kelvin": 5555, + }, + ), + ), + light.DEFAULT_MIN_KELVIN, + 5555, + ), + ( + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ( + { + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "min_kelvin": 2500, + }, + ), + ), + 2500, + light.DEFAULT_MAX_KELVIN, + ), + ], +) +async def test_no_min_max_kelvin( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + min_kelvin: int, + max_kelvin: int, +) -> None: + """Test if there is no color and brightness if no topic.""" + await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test-topic", "ON") + state = hass.states.get("light.test") + assert state is not None and state.state == STATE_UNKNOWN + assert state.attributes.get(light.ATTR_MIN_COLOR_TEMP_KELVIN) == min_kelvin + assert state.attributes.get(light.ATTR_MAX_COLOR_TEMP_KELVIN) == max_kelvin + + @pytest.mark.parametrize( "hass_config", [ @@ -431,6 +543,76 @@ async def test_controlling_state_via_topic( assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes +@pytest.mark.parametrize( + ("hass_config", "payload", "kelvin"), + [ + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "name": "test", + "state_topic": "test_light_color_temp/status", + "command_topic": "test_light_color_temp/set", + "brightness_state_topic": "test_light_color_temp/brightness/status", + "brightness_command_topic": "test_light_color_temp/brightness/set", + "color_temp_state_topic": "test_light_color_temp/color_temp/status", + "color_temp_command_topic": "test_light_color_temp/color_temp/set", + "color_temp_kelvin": False, + } + } + }, + "300", + 3333, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "name": "test", + "state_topic": "test_light_color_temp/status", + "command_topic": "test_light_color_temp/set", + "brightness_state_topic": "test_light_color_temp/brightness/status", + "brightness_command_topic": "test_light_color_temp/brightness/set", + "color_temp_state_topic": "test_light_color_temp/color_temp/status", + "color_temp_command_topic": "test_light_color_temp/color_temp/set", + "color_temp_kelvin": True, + } + } + }, + "3333", + 3333, + ), + ], + ids=["mireds", "kelvin"], +) +async def test_controlling_color_mode_state_via_topic( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + payload: str, + kelvin: int, +) -> None: + """Test the controlling of the color mode state via topic.""" + color_modes = ["color_temp"] + + await mqtt_mock_entry() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get("color_temp_kelvin") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_color_temp/status", "ON") + async_fire_mqtt_message(hass, "test_light_color_temp/brightness/status", "70") + async_fire_mqtt_message(hass, "test_light_color_temp/color_temp/status", payload) + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") == 70 + assert light_state.attributes["color_temp_kelvin"] == kelvin + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + @pytest.mark.parametrize( "hass_config", [ @@ -1295,25 +1477,47 @@ async def test_sending_mqtt_rgbww_command_with_template( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "payload"), [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "name": "test", - "command_topic": "test_light_color_temp/set", - "color_temp_command_topic": "test_light_color_temp/color_temp/set", - "color_temp_command_template": "{{ (1000 / value) | round(0) }}", - "payload_on": "on", - "payload_off": "off", - "qos": 0, + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "name": "test", + "command_topic": "test_light_color_temp/set", + "color_temp_command_topic": "test_light_color_temp/color_temp/set", + "color_temp_command_template": "{{ (1000 / value) | round(0) }}", + "color_temp_kelvin": False, + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } } - } - } + }, + "10", + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "name": "test", + "command_topic": "test_light_color_temp/set", + "color_temp_command_topic": "test_light_color_temp/color_temp/set", + "color_temp_command_template": "{{ (0.5 * value) | round(0) }}", + "color_temp_kelvin": True, + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + }, + "5000", + ), ], + ids=["mireds", "kelvin"], ) async def test_sending_mqtt_color_temp_command_with_template( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, payload: str ) -> None: """Test the sending of Color Temp command with template.""" mqtt_mock = await mqtt_mock_entry() @@ -1326,14 +1530,14 @@ async def test_sending_mqtt_color_temp_command_with_template( mqtt_mock.async_publish.assert_has_calls( [ call("test_light_color_temp/set", "on", 0, False), - call("test_light_color_temp/color_temp/set", "10", 0, False), + call("test_light_color_temp/color_temp/set", payload, 0, False), ], any_order=True, ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes["color_temp"] == 100 + assert state.attributes["color_temp_kelvin"] == 10000 @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 988cce85653..7ddd04a09a6 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -14,7 +14,7 @@ mqtt: rgb: true xy: true -Configuration with RGB, brightness, color temp and effect: +Configuration with RGB, brightness, color temp (mireds) and effect: mqtt: light: @@ -24,10 +24,11 @@ mqtt: command_topic: "home/rgb1/set" brightness: true color_temp: true + color_temp_kelvin: false effect: true rgb: true -Configuration with RGB, brightness and color temp: +Configuration with RGB, brightness and color temp (Kelvin): mqtt: light: @@ -38,6 +39,7 @@ mqtt: brightness: true rgb: true color_temp: true + color_temp_kelvin: true Configuration with RGB, brightness: @@ -98,7 +100,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.json import json_dumps from homeassistant.util.json import json_loads from .test_common import ( @@ -193,172 +194,6 @@ async def test_fail_setup_if_no_command_topic( assert "required key not provided" in caplog.text -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config(light.DOMAIN, COLOR_MODES_CONFIG, ({"color_temp": True},)), - help_custom_config(light.DOMAIN, COLOR_MODES_CONFIG, ({"hs": True},)), - help_custom_config(light.DOMAIN, COLOR_MODES_CONFIG, ({"rgb": True},)), - help_custom_config(light.DOMAIN, COLOR_MODES_CONFIG, ({"xy": True},)), - ], -) -async def test_fail_setup_if_color_mode_deprecated( - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test if setup fails if color mode is combined with deprecated config keys.""" - assert await mqtt_mock_entry() - assert "supported_color_modes must not be combined with any of" in caplog.text - - -@pytest.mark.parametrize( - ("hass_config", "color_modes"), - [ - ( - help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True},)), - ("color_temp",), - ), - (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"hs": True},)), ("hs",)), - (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"rgb": True},)), ("rgb",)), - (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"xy": True},)), ("xy",)), - ( - help_custom_config( - light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True, "rgb": True},) - ), - ("color_temp, rgb", "rgb, color_temp"), - ), - ], - ids=["color_temp", "hs", "rgb", "xy", "color_temp, rgb"], -) -async def test_warning_if_color_mode_flags_are_used( - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, - color_modes: tuple[str, ...], -) -> None: - """Test warnings deprecated config keys without supported color modes defined.""" - with patch( - "homeassistant.components.mqtt.light.schema_json.async_create_issue" - ) as mock_async_create_issue: - assert await mqtt_mock_entry() - assert any( - ( - f"Deprecated flags [{color_modes_case}] used in MQTT JSON light config " - "for handling color mode, please use `supported_color_modes` instead." - in caplog.text - ) - for color_modes_case in color_modes - ) - mock_async_create_issue.assert_called_once() - - -@pytest.mark.parametrize( - ("config", "color_modes"), - [ - ( - help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True},)), - ("color_temp",), - ), - (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"hs": True},)), ("hs",)), - (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"rgb": True},)), ("rgb",)), - (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"xy": True},)), ("xy",)), - ( - help_custom_config( - light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True, "rgb": True},) - ), - ("color_temp, rgb", "rgb, color_temp"), - ), - ], - ids=["color_temp", "hs", "rgb", "xy", "color_temp, rgb"], -) -async def test_warning_on_discovery_if_color_mode_flags_are_used( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, - config: dict[str, Any], - color_modes: tuple[str, ...], -) -> None: - """Test warnings deprecated config keys with discovery.""" - with patch( - "homeassistant.components.mqtt.light.schema_json.async_create_issue" - ) as mock_async_create_issue: - assert await mqtt_mock_entry() - - config_payload = json_dumps(config[mqtt.DOMAIN][light.DOMAIN][0]) - async_fire_mqtt_message( - hass, - "homeassistant/light/bla/config", - config_payload, - ) - await hass.async_block_till_done() - assert any( - ( - f"Deprecated flags [{color_modes_case}] used in MQTT JSON light config " - "for handling color mode, please " - "use `supported_color_modes` instead" in caplog.text - ) - for color_modes_case in color_modes - ) - mock_async_create_issue.assert_not_called() - - -@pytest.mark.parametrize( - "hass_config", - [ - help_custom_config( - light.DOMAIN, - DEFAULT_CONFIG, - ({"color_mode": True, "supported_color_modes": ["color_temp"]},), - ), - ], - ids=["color_temp"], -) -async def test_warning_if_color_mode_option_flag_is_used( - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test warning deprecated color_mode option flag is used.""" - with patch( - "homeassistant.components.mqtt.light.schema_json.async_create_issue" - ) as mock_async_create_issue: - assert await mqtt_mock_entry() - assert "Deprecated flag `color_mode` used in MQTT JSON light config" in caplog.text - mock_async_create_issue.assert_called_once() - - -@pytest.mark.parametrize( - "config", - [ - help_custom_config( - light.DOMAIN, - DEFAULT_CONFIG, - ({"color_mode": True, "supported_color_modes": ["color_temp"]},), - ), - ], - ids=["color_temp"], -) -async def test_warning_on_discovery_if_color_mode_option_flag_is_used( - hass: HomeAssistant, - mqtt_mock_entry: MqttMockHAClientGenerator, - caplog: pytest.LogCaptureFixture, - config: dict[str, Any], -) -> None: - """Test warning deprecated color_mode option flag is used.""" - with patch( - "homeassistant.components.mqtt.light.schema_json.async_create_issue" - ) as mock_async_create_issue: - assert await mqtt_mock_entry() - - config_payload = json_dumps(config[mqtt.DOMAIN][light.DOMAIN][0]) - async_fire_mqtt_message( - hass, - "homeassistant/light/bla/config", - config_payload, - ) - await hass.async_block_till_done() - assert "Deprecated flag `color_mode` used in MQTT JSON light config" in caplog.text - mock_async_create_issue.assert_not_called() - - @pytest.mark.parametrize( ("hass_config", "error"), [ @@ -398,50 +233,6 @@ async def test_fail_setup_if_color_modes_invalid( assert error in caplog.text -@pytest.mark.parametrize( - "hass_config", - [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_light/set", - "state_topic": "test_light", - "color_mode": True, - "supported_color_modes": "color_temp", - } - } - } - ], -) -async def test_single_color_mode( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test setup with single color_mode.""" - await mqtt_mock_entry() - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - - await common.async_turn_on( - hass, "light.test", brightness=50, color_temp_kelvin=5208 - ) - - async_fire_mqtt_message( - hass, - "test_light", - '{"state": "ON", "brightness": 50, "color_mode": "color_temp", "color_temp": 192}', - ) - color_modes = [light.ColorMode.COLOR_TEMP] - state = hass.states.get("light.test") - assert state.state == STATE_ON - - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - assert state.attributes.get(light.ATTR_COLOR_TEMP_KELVIN) == 5208 - assert state.attributes.get(light.ATTR_BRIGHTNESS) == 50 - assert state.attributes.get(light.ATTR_COLOR_MODE) == color_modes[0] - - @pytest.mark.parametrize("hass_config", [COLOR_MODES_CONFIG]) async def test_turn_on_with_unknown_color_mode_optimistic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator @@ -516,34 +307,6 @@ async def test_controlling_state_with_unknown_color_mode( assert state.attributes.get(light.ATTR_COLOR_MODE) == light.ColorMode.COLOR_TEMP -@pytest.mark.parametrize( - "hass_config", - [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_light_rgb/set", - "rgb": True, - } - } - } - ], -) -async def test_legacy_rgb_light( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test legacy RGB light flags expected features and color modes.""" - await mqtt_mock_entry() - - state = hass.states.get("light.test") - color_modes = [light.ColorMode.HS] - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features - - @pytest.mark.parametrize( "hass_config", [ @@ -608,22 +371,19 @@ async def test_no_color_brightness_color_temp_if_no_topics( "name": "test", "state_topic": "test_light_rgb", "command_topic": "test_light_rgb/set", - "brightness": True, - "color_temp": True, + "color_temp_kelvin": True, "effect": True, - "rgb": True, - "xy": True, - "hs": True, + "supported_color_modes": ["color_temp", "hs"], "qos": "0", } } } ], ) -async def test_controlling_state_via_topic( +async def test_controlling_state_color_temp_kelvin( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: - """Test the controlling of the state via topic.""" + """Test the controlling of the state via topic in Kelvin mode.""" await mqtt_mock_entry() state = hass.states.get("light.test") @@ -631,9 +391,11 @@ async def test_controlling_state_via_topic( color_modes = [light.ColorMode.COLOR_TEMP, light.ColorMode.HS] assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes expected_features = ( - light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + light.LightEntityFeature.EFFECT + | light.LightEntityFeature.FLASH + | light.LightEntityFeature.TRANSITION ) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) is expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp_kelvin") is None @@ -647,7 +409,8 @@ async def test_controlling_state_via_topic( hass, "test_light_rgb", '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255},' + '"color":{"h": 44.098, "s": 2.43},' + '"color_mode": "hs",' '"brightness":255,' '"color_temp":155,' '"effect":"colorloop"}', @@ -655,12 +418,12 @@ async def test_controlling_state_via_topic( state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("rgb_color") == (255, 253, 249) assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority assert state.attributes.get("effect") == "colorloop" - assert state.attributes.get("xy_color") == (0.323, 0.329) - assert state.attributes.get("hs_color") == (0.0, 0.0) + assert state.attributes.get("xy_color") == (0.328, 0.333) + assert state.attributes.get("hs_color") == (44.098, 2.43) # Turn on the light async_fire_mqtt_message( @@ -669,7 +432,8 @@ async def test_controlling_state_via_topic( '{"state":"ON",' '"brightness":255,' '"color":null,' - '"color_temp":155,' + '"color_mode":"color_temp",' + '"color_temp":6451,' # Kelvin '"effect":"colorloop"}', ) @@ -686,92 +450,6 @@ async def test_controlling_state_via_topic( assert state.attributes.get("xy_color") == (0.328, 0.333) # temp converted to color assert state.attributes.get("hs_color") == (44.098, 2.43) # temp converted to color - # Turn the light off - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"OFF"}') - - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "brightness":100}') - - light_state = hass.states.get("light.test") - - assert light_state.attributes["brightness"] == 100 - - async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", "color":{"r":125,"g":125,"b":125}}' - ) - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 255, 255) - - async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", "color":{"x":0.135,"y":0.135}}' - ) - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("xy_color") == (0.141, 0.141) - - async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", "color":{"h":180,"s":50}}' - ) - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (180.0, 50.0) - - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color":null}') - - light_state = hass.states.get("light.test") - assert "hs_color" in light_state.attributes # Color temp approximation - - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":155}') - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("color_temp") == 155 - - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":null}') - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("color_temp") is None - - async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", "effect":"colorloop"}' - ) - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("effect") == "colorloop" - - async_fire_mqtt_message( - hass, - "test_light_rgb", - '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255},' - '"brightness":128,' - '"color_temp":155,' - '"effect":"colorloop"}', - ) - light_state = hass.states.get("light.test") - assert light_state.state == STATE_ON - assert light_state.attributes.get("brightness") == 128 - - async_fire_mqtt_message( - hass, - "test_light_rgb", - '{"state":"OFF","brightness":0}', - ) - light_state = hass.states.get("light.test") - assert light_state.state == STATE_OFF - assert light_state.attributes.get("brightness") is None - - # test previous zero brightness received was ignored and brightness is restored - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON"}') - light_state = hass.states.get("light.test") - assert light_state.attributes.get("brightness") == 128 - - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON","brightness":0}') - light_state = hass.states.get("light.test") - assert light_state.attributes.get("brightness") == 128 - @pytest.mark.parametrize( "hass_config", @@ -781,7 +459,7 @@ async def test_controlling_state_via_topic( ) ], ) -async def test_controlling_state_via_topic2( +async def test_controlling_state_via_topic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture, @@ -842,6 +520,11 @@ async def test_controlling_state_via_topic2( state = hass.states.get("light.test") assert state.attributes["brightness"] == 100 + # Zero brightness value is ignored + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "brightness":0}') + state = hass.states.get("light.test") + assert state.attributes["brightness"] == 100 + # RGB color async_fire_mqtt_message( hass, @@ -944,242 +627,6 @@ async def test_controlling_state_via_topic2( { mqtt.DOMAIN: { light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_light_rgb/set", - "state_topic": "test_light_rgb/set", - "rgb": True, - "color_temp": True, - "brightness": True, - } - } - } - ], -) -async def test_controlling_the_state_with_legacy_color_handling( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test state updates for lights with a legacy color handling.""" - supported_color_modes = ["color_temp", "hs"] - await mqtt_mock_entry() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - expected_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_mode") is None - assert state.attributes.get("color_temp_kelvin") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("supported_color_modes") == supported_color_modes - assert state.attributes.get("xy_color") is None - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - for _ in range(2): - # Returned state after the light was turned on - # Receiving legacy color mode: rgb. - async_fire_mqtt_message( - hass, - "test_light_rgb/set", - '{ "state": "ON", "brightness": 255, "level": 100, "hue": 16,' - '"saturation": 100, "color": { "r": 255, "g": 67, "b": 0 }, ' - '"bulb_mode": "color", "color_mode": "rgb" }', - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_mode") == "hs" - assert state.attributes.get("color_temp_kelvin") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") == (15.765, 100.0) - assert state.attributes.get("rgb_color") == (255, 67, 0) - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("xy_color") == (0.674, 0.322) - - # Returned state after the lights color mode was changed - # Receiving legacy color mode: color_temp - async_fire_mqtt_message( - hass, - "test_light_rgb/set", - '{ "state": "ON", "brightness": 255, "level": 100, ' - '"kelvin": 92, "color_temp": 353, "bulb_mode": "white", ' - '"color_mode": "color_temp" }', - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_mode") == "color_temp" - assert state.attributes.get("color_temp_kelvin") == 2832 - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") == (28.125, 61.661) - assert state.attributes.get("rgb_color") == (255, 171, 98) - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("xy_color") == (0.512, 0.385) - - -@pytest.mark.parametrize( - "hass_config", - [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_light_rgb/set", - "brightness": True, - "color_temp": True, - "effect": True, - "hs": True, - "rgb": True, - "xy": True, - "qos": 2, - } - } - } - ], -) -async def test_sending_mqtt_commands_and_optimistic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator -) -> None: - """Test the sending of command in optimistic mode.""" - fake_state = State( - "light.test", - "on", - { - "brightness": 95, - "hs_color": [100, 100], - "effect": "random", - "color_temp_kelvin": 10000, - }, - ) - mock_restore_cache(hass, (fake_state,)) - - mqtt_mock = await mqtt_mock_entry() - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 95 - assert state.attributes.get("hs_color") == (100, 100) - assert state.attributes.get("effect") == "random" - assert state.attributes.get("color_temp_kelvin") is None # hs_color has priority - color_modes = [light.ColorMode.COLOR_TEMP, light.ColorMode.HS] - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - expected_features = ( - light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - ) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features - assert state.attributes.get(ATTR_ASSUMED_STATE) - - await common.async_turn_on(hass, "light.test") - - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", '{"state":"ON"}', 2, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_ON - - await common.async_turn_on(hass, "light.test", color_temp_kelvin=11111) - - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", - JsonValidator('{"state": "ON", "color_temp": 90}'), - 2, - False, - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("color_mode") == light.ColorMode.COLOR_TEMP - assert state.attributes.get("color_temp_kelvin") == 11111 - - await common.async_turn_off(hass, "light.test") - - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", '{"state":"OFF"}', 2, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - mqtt_mock.reset_mock() - await common.async_turn_on( - hass, "light.test", brightness=50, xy_color=(0.123, 0.123) - ) - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 0, "g": 124, "b": 255,' - ' "x": 0.14, "y": 0.133, "h": 210.824, "s": 100.0},' - ' "brightness": 50}' - ), - 2, - False, - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.attributes.get("color_mode") == light.ColorMode.HS - assert state.attributes["brightness"] == 50 - assert state.attributes["hs_color"] == (210.824, 100.0) - assert state.attributes["rgb_color"] == (0, 124, 255) - assert state.attributes["xy_color"] == (0.14, 0.133) - - await common.async_turn_on(hass, "light.test", brightness=50, hs_color=(359, 78)) - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 56, "b": 59,' - ' "x": 0.654, "y": 0.301, "h": 359.0, "s": 78.0},' - ' "brightness": 50}' - ), - 2, - False, - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("color_mode") == light.ColorMode.HS - assert state.attributes["brightness"] == 50 - assert state.attributes["hs_color"] == (359.0, 78.0) - assert state.attributes["rgb_color"] == (255, 56, 59) - assert state.attributes["xy_color"] == (0.654, 0.301) - - await common.async_turn_on(hass, "light.test", rgb_color=(255, 128, 0)) - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0,' - ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0}}' - ), - 2, - False, - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("color_mode") == light.ColorMode.HS - assert state.attributes["brightness"] == 50 - assert state.attributes["hs_color"] == (30.118, 100) - assert state.attributes["rgb_color"] == (255, 128, 0) - assert state.attributes["xy_color"] == (0.611, 0.375) - - -@pytest.mark.parametrize( - "hass_config", - [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "brightness": True, - "color_mode": True, "command_topic": "test_light_rgb/set", "effect": True, "name": "test", @@ -1199,7 +646,7 @@ async def test_sending_mqtt_commands_and_optimistic( } ], ) -async def test_sending_mqtt_commands_and_optimistic2( +async def test_sending_mqtt_commands_and_optimistic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: """Test the sending of command in optimistic mode for a light supporting color mode.""" @@ -1421,8 +868,7 @@ async def test_sending_mqtt_commands_and_optimistic2( "schema": "json", "name": "test", "command_topic": "test_light_rgb/set", - "brightness": True, - "hs": True, + "supported_color_modes": ["hs"], } } } @@ -1484,7 +930,7 @@ async def test_sending_hs_color( "schema": "json", "name": "test", "command_topic": "test_light_rgb/set", - "rgb": True, + "supported_color_modes": ["rgb"], } } } @@ -1539,7 +985,6 @@ async def test_sending_rgb_color_no_brightness( { mqtt.DOMAIN: { light.DOMAIN: { - "color_mode": True, "command_topic": "test_light_rgb/set", "name": "test", "schema": "json", @@ -1622,8 +1067,8 @@ async def test_sending_rgb_color_no_brightness2( "schema": "json", "name": "test", "command_topic": "test_light_rgb/set", + "supported_color_modes": ["rgb"], "brightness": True, - "rgb": True, } } } @@ -1690,9 +1135,9 @@ async def test_sending_rgb_color_with_brightness( "schema": "json", "name": "test", "command_topic": "test_light_rgb/set", + "supported_color_modes": ["rgb"], "brightness": True, "brightness_scale": 100, - "rgb": True, } } } @@ -1760,9 +1205,7 @@ async def test_sending_rgb_color_with_scaled_brightness( "schema": "json", "name": "test", "command_topic": "test_light_rgb/set", - "brightness": True, "brightness_scale": 100, - "color_mode": True, "supported_color_modes": ["hs", "white"], "white_scale": 50, } @@ -1807,8 +1250,7 @@ async def test_sending_scaled_white( "schema": "json", "name": "test", "command_topic": "test_light_rgb/set", - "brightness": True, - "xy": True, + "supported_color_modes": ["xy"], } } } @@ -1834,7 +1276,7 @@ async def test_sending_xy_color( call( "test_light_rgb/set", JsonValidator( - '{"state": "ON", "color": {"x": 0.14, "y": 0.133},' + '{"state": "ON", "color": {"x": 0.123, "y": 0.123},' ' "brightness": 50}' ), 0, @@ -2051,7 +1493,7 @@ async def test_transition( "name": "test", "state_topic": "test_light_bright_scale", "command_topic": "test_light_bright_scale/set", - "brightness": True, + "supported_color_modes": ["brightness"], "brightness_scale": 99, } } @@ -2116,7 +1558,6 @@ async def test_brightness_scale( "command_topic": "test_light_bright_scale/set", "brightness": True, "brightness_scale": 99, - "color_mode": True, "supported_color_modes": ["hs", "white"], "white_scale": 50, } @@ -2176,8 +1617,7 @@ async def test_white_scale( "state_topic": "test_light_rgb", "command_topic": "test_light_rgb/set", "brightness": True, - "color_temp": True, - "rgb": True, + "supported_color_modes": ["hs", "color_temp"], "qos": "0", } } @@ -2210,62 +1650,64 @@ async def test_invalid_values( '{"state":"ON",' '"color":{"r":255,"g":255,"b":255},' '"brightness": 255,' + '"color_mode": "color_temp",' '"color_temp": 100,' '"effect": "rainbow"}', ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) + # Color converttrd from color_temp to rgb + assert state.attributes.get("rgb_color") == (202, 218, 255) assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp_kelvin") is None + assert state.attributes.get("color_temp_kelvin") == 10000 # Empty color value async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON", "color":{}}', + '{"state":"ON", "color":{}, "color_mode": "rgb"}', ) # Color should not have changed state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("rgb_color") == (202, 218, 255) # Bad HS color values async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON", "color":{"h":"bad","s":"val"}}', + '{"state":"ON", "color":{"h":"bad","s":"val"}, "color_mode": "hs"}', ) # Color should not have changed state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("rgb_color") == (202, 218, 255) # Bad RGB color values async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON", "color":{"r":"bad","g":"val","b":"test"}}', + '{"state":"ON", "color":{"r":"bad","g":"val","b":"test"}, "color_mode": "rgb"}', ) # Color should not have changed state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("rgb_color") == (202, 218, 255) # Bad XY color values async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON", "color":{"x":"bad","y":"val"}}', + '{"state":"ON", "color":{"x":"bad","y":"val"}, "color_mode": "xy"}', ) # Color should not have changed state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("rgb_color") == (202, 218, 255) # Bad brightness values async_fire_mqtt_message( @@ -2279,7 +1721,9 @@ async def test_invalid_values( # Unset color and set a valid color temperature async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", "color": null, "color_temp": 100}' + hass, + "test_light_rgb", + '{"state":"ON", "color": null, "color_temp": 100, "color_mode": "color_temp"}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -2287,11 +1731,14 @@ async def test_invalid_values( # Bad color temperature async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", "color_temp": "badValue"}' + hass, + "test_light_rgb", + '{"state":"ON", "color_temp": "badValue", "color_mode": "color_temp"}', ) assert ( - "Invalid color temp value 'badValue' received for entity light.test" - in caplog.text + "Invalid or incomplete color value '{'state': 'ON', 'color_temp': " + "'badValue', 'color_mode': 'color_temp'}' " + "received for entity light.test" in caplog.text ) # Color temperature should not have changed @@ -2576,30 +2023,82 @@ async def test_entity_debug_info_message( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "min_kelvin", "max_kelvin"), [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_max_mireds/set", - "color_temp": True, - "max_mireds": 370, + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "max_mireds": 370, # 2702 Kelvin + } } - } - } + }, + 2702, + light.DEFAULT_MAX_KELVIN, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "min_mireds": 150, # 6666 Kelvin + } + } + }, + light.DEFAULT_MIN_KELVIN, + 6666, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "min_kelvin": 2702, + } + } + }, + 2702, + light.DEFAULT_MAX_KELVIN, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "max_kelvin": 6666, + } + } + }, + light.DEFAULT_MIN_KELVIN, + 6666, + ), ], ) -async def test_max_mireds( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +async def test_min_max_kelvin( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + min_kelvin: int, + max_kelvin: int, ) -> None: - """Test setting min_mireds and max_mireds.""" + """Test setting min_color_temp_kelvin and max_color_temp_kelvin.""" await mqtt_mock_entry() state = hass.states.get("light.test") - assert state.attributes.get("min_mireds") == 153 - assert state.attributes.get("max_mireds") == 370 + assert state.attributes.get("min_color_temp_kelvin") == min_kelvin + assert state.attributes.get("max_color_temp_kelvin") == max_kelvin @pytest.mark.parametrize( @@ -2736,7 +2235,6 @@ async def test_setup_manual_entity_from_yaml( DEFAULT_CONFIG, ( { - "color_mode": True, "effect": True, "supported_color_modes": [ "color_temp", diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 4d2b93ff159..568d86f8bd9 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -179,25 +179,50 @@ async def test_rgb_light( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "kelvin", "payload"), [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "template", - "name": "test", - "command_topic": "test_light/set", - "command_on_template": "on,{{ brightness|d }},{{ color_temp|d }}", - "command_off_template": "off", - "brightness_template": "{{ value.split(',')[1] }}", - "color_temp_template": "{{ value.split(',')[2] }}", + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test_light/set", + "command_on_template": "on,{{ brightness|d }},{{ color_temp|d }}", + "command_off_template": "off", + "brightness_template": "{{ value.split(',')[1] }}", + "color_temp_template": "{{ value.split(',')[2] }}", + } } - } - } + }, + 5208, + "192", + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test_light/set", + "command_on_template": "on,{{ brightness|d }},{{ color_temp|d }}", + "command_off_template": "off", + "brightness_template": "{{ value.split(',')[1] }}", + "color_temp_template": "{{ value.split(',')[2] }}", + } + } + }, + 5208, + "5208", + ), ], + ids=["mireds", "kelvin"], ) async def test_single_color_mode( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + kelvin: int, + payload: str, ) -> None: """Test the color mode when we only have one supported color_mode.""" await mqtt_mock_entry() @@ -206,15 +231,15 @@ async def test_single_color_mode( assert state.state == STATE_UNKNOWN await common.async_turn_on( - hass, "light.test", brightness=50, color_temp_kelvin=5208 + hass, "light.test", brightness=50, color_temp_kelvin=kelvin ) - async_fire_mqtt_message(hass, "test_light", "on,50,192") + async_fire_mqtt_message(hass, "test_light", f"on,50,{payload}") color_modes = [light.ColorMode.COLOR_TEMP] state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - assert state.attributes.get(light.ATTR_COLOR_TEMP_KELVIN) == 5208 + assert state.attributes.get(light.ATTR_COLOR_TEMP_KELVIN) == kelvin assert state.attributes.get(light.ATTR_BRIGHTNESS) == 50 assert state.attributes.get(light.ATTR_COLOR_MODE) == color_modes[0] @@ -392,39 +417,80 @@ async def test_state_brightness_color_effect_temp_change_via_topic( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "kelvin", "payload"), [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "template", - "name": "test", - "command_topic": "test_light_rgb/set", - "command_on_template": "on," - "{{ brightness|d }}," - "{{ color_temp|d }}," - "{{ red|d }}-" - "{{ green|d }}-" - "{{ blue|d }}," - "{{ hue|d }}-" - "{{ sat|d }}", - "command_off_template": "off", - "effect_list": ["colorloop", "random"], - "optimistic": True, - "state_template": '{{ value.split(",")[0] }}', - "color_temp_template": '{{ value.split(",")[2] }}', - "red_template": '{{ value.split(",")[3].split("-")[0] }}', - "green_template": '{{ value.split(",")[3].split("-")[1] }}', - "blue_template": '{{ value.split(",")[3].split("-")[2] }}', - "effect_template": '{{ value.split(",")[4] }}', - "qos": 2, + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on," + "{{ brightness|d }}," + "{{ color_temp|d }}," + "{{ red|d }}-" + "{{ green|d }}-" + "{{ blue|d }}," + "{{ hue|d }}-" + "{{ sat|d }}", + "command_off_template": "off", + "effect_list": ["colorloop", "random"], + "optimistic": True, + "state_template": '{{ value.split(",")[0] }}', + "color_temp_kelvin": False, + "color_temp_template": '{{ value.split(",")[2] }}', + "red_template": '{{ value.split(",")[3].split("-")[0] }}', + "green_template": '{{ value.split(",")[3].split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', + "qos": 2, + } } - } - } + }, + 14285, + "on,,70,--,-", + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "template", + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on," + "{{ brightness|d }}," + "{{ color_temp|d }}," + "{{ red|d }}-" + "{{ green|d }}-" + "{{ blue|d }}," + "{{ hue|d }}-" + "{{ sat|d }}", + "command_off_template": "off", + "effect_list": ["colorloop", "random"], + "optimistic": True, + "state_template": '{{ value.split(",")[0] }}', + "color_temp_kelvin": True, + "color_temp_template": '{{ value.split(",")[2] }}', + "red_template": '{{ value.split(",")[3].split("-")[0] }}', + "green_template": '{{ value.split(",")[3].split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', + "qos": 2, + } + }, + }, + 14285, + "on,,14285,--,-", + ), ], + ids=["mireds", "kelvin"], ) async def test_sending_mqtt_commands_and_optimistic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + kelvin: int, + payload: str, ) -> None: """Test the sending of command in optimistic mode.""" fake_state = State( @@ -465,14 +531,15 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.state == STATE_ON # Set color_temp - await common.async_turn_on(hass, "light.test", color_temp_kelvin=14285) + await common.async_turn_on(hass, "light.test", color_temp_kelvin=kelvin) + # Assert mireds or Kelvin as payload mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,70,--,-", 2, False + "test_light_rgb/set", payload, 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("color_temp_kelvin") == 14285 + assert state.attributes.get("color_temp_kelvin") == kelvin # Set full brightness await common.async_turn_on(hass, "light.test", brightness=255) diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 5b7984cad62..d65f1a4d661 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -313,7 +313,12 @@ async def test_default_entity_and_device_name( hass.set_state(CoreState.starting) await hass.async_block_till_done() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "mock-broker"}) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={mqtt.CONF_BROKER: "mock-broker"}, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, + ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 48aaa11f672..7bdd39e81a7 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -29,6 +29,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .test_common import ( help_custom_config, @@ -157,6 +158,101 @@ async def test_run_number_setup( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": "test/state_number", + "command_topic": "test/cmd_number", + "name": "Test Number", + "min": 15, + "max": 28, + "device_class": "temperature", + "unit_of_measurement": UnitOfTemperature.CELSIUS.value, + } + } + } + ], +) +async def test_native_value_validation( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test state validation and native value conversion.""" + mqtt_mock = await mqtt_mock_entry() + + async_fire_mqtt_message(hass, "test/state_number", "23.5") + state = hass.states.get("number.test_number") + assert state is not None + assert state.attributes.get(ATTR_MIN) == 15 + assert state.attributes.get(ATTR_MAX) == 28 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfTemperature.CELSIUS.value + ) + assert state.state == "23.5" + + # Test out of range validation + async_fire_mqtt_message(hass, "test/state_number", "29.5") + state = hass.states.get("number.test_number") + assert state is not None + assert state.attributes.get(ATTR_MIN) == 15 + assert state.attributes.get(ATTR_MAX) == 28 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfTemperature.CELSIUS.value + ) + assert state.state == "23.5" + assert ( + "Invalid value for number.test_number: 29.5 (range 15.0 - 28.0)" in caplog.text + ) + caplog.clear() + + # Check if validation still works when changing unit system + hass.config.units = US_CUSTOMARY_SYSTEM + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test/state_number", "24.5") + state = hass.states.get("number.test_number") + assert state is not None + assert state.attributes.get(ATTR_MIN) == 59.0 + assert state.attributes.get(ATTR_MAX) == 82.4 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfTemperature.FAHRENHEIT.value + ) + assert state.state == "76.1" + + # Test out of range validation again + async_fire_mqtt_message(hass, "test/state_number", "29.5") + state = hass.states.get("number.test_number") + assert state is not None + assert state.attributes.get(ATTR_MIN) == 59.0 + assert state.attributes.get(ATTR_MAX) == 82.4 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfTemperature.FAHRENHEIT.value + ) + assert state.state == "76.1" + assert ( + "Invalid value for number.test_number: 29.5 (range 15.0 - 28.0)" in caplog.text + ) + caplog.clear() + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 68}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with("test/cmd_number", "20", 0, False) + mqtt_mock.async_publish.reset_mock() + + @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 7f418864872..6b3bbd6334c 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -23,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .test_common import ( help_custom_config, diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 37bf6982b7a..f751096bca2 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -4,7 +4,6 @@ import asyncio from collections.abc import Callable from datetime import timedelta from pathlib import Path -from random import getrandbits import shutil import tempfile from unittest.mock import MagicMock, patch @@ -53,7 +52,7 @@ async def test_canceling_debouncer_on_shutdown( assert not mock_debouncer.is_set() mqtt_client_mock.subscribe.assert_not_called() - # Note thet the broker connection will not be disconnected gracefully + # Note that the broker connection will not be disconnected gracefully await hass.async_block_till_done() async_fire_time_changed(hass, utcnow() + timedelta(seconds=5)) await asyncio.sleep(0) @@ -199,7 +198,6 @@ async def test_reading_non_exitisting_certificate_file() -> None: ) -@pytest.mark.parametrize("temp_dir_prefix", "unknown") async def test_return_default_get_file_path( hass: HomeAssistant, mock_temp_dir: str ) -> None: @@ -211,12 +209,8 @@ async def test_return_default_get_file_path( and mqtt.util.get_file_path("some_option", "mydefault") == "mydefault" ) - with patch( - "homeassistant.components.mqtt.util.TEMP_DIR_NAME", - f"home-assistant-mqtt-other-{getrandbits(10):03x}", - ) as temp_dir_name: - tempdir = Path(tempfile.gettempdir()) / temp_dir_name - assert await hass.async_add_executor_job(_get_file_path, tempdir) + temp_dir = Path(tempfile.gettempdir()) / mock_temp_dir + assert await hass.async_add_executor_job(_get_file_path, temp_dir) async def test_waiting_for_client_not_loaded( @@ -231,6 +225,8 @@ async def test_waiting_for_client_not_loaded( domain=mqtt.DOMAIN, data={"broker": "test-broker"}, state=ConfigEntryState.NOT_LOADED, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) @@ -286,6 +282,8 @@ async def test_waiting_for_client_entry_fails( domain=mqtt.DOMAIN, data={"broker": "test-broker"}, state=ConfigEntryState.NOT_LOADED, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) @@ -314,6 +312,8 @@ async def test_waiting_for_client_setup_fails( domain=mqtt.DOMAIN, data={"broker": "test-broker"}, state=ConfigEntryState.NOT_LOADED, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) @@ -341,6 +341,8 @@ async def test_waiting_for_client_timeout( domain=mqtt.DOMAIN, data={"broker": "test-broker"}, state=ConfigEntryState.NOT_LOADED, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) @@ -360,6 +362,8 @@ async def test_waiting_for_client_with_disabled_entry( domain=mqtt.DOMAIN, data={"broker": "test-broker"}, state=ConfigEntryState.NOT_LOADED, + version=mqtt.CONFIG_ENTRY_VERSION, + minor_version=mqtt.CONFIG_ENTRY_MINOR_VERSION, ) entry.add_to_hass(hass) diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index b6c1940b149..cbf02299b09 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -5,12 +5,12 @@ from unittest.mock import ANY, patch import pytest -import homeassistant.components.mqtt_eventstream as eventstream +from homeassistant.components import mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( async_fire_mqtt_message, diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index 9798477945c..63c3ea14e44 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import ANY, call import pytest -import homeassistant.components.mqtt_statestream as statestream +from homeassistant.components import mqtt_statestream as statestream from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.setup import async_setup_component diff --git a/tests/components/music_assistant/common.py b/tests/components/music_assistant/common.py index c8293b5622f..7c0f9df751a 100644 --- a/tests/components/music_assistant/common.py +++ b/tests/components/music_assistant/common.py @@ -30,7 +30,7 @@ def load_and_parse_fixture(fixture: str) -> dict[str, Any]: async def setup_integration_from_fixtures( hass: HomeAssistant, music_assistant_client: MagicMock, -) -> None: +) -> MockConfigEntry: """Set up MusicAssistant integration with fixture data.""" players = create_players_from_fixture() music_assistant_client.players._players = {x.player_id: x for x in players} @@ -65,6 +65,7 @@ async def setup_integration_from_fixtures( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + return config_entry def create_players_from_fixture() -> list[Player]: diff --git a/tests/components/music_assistant/fixtures/players.json b/tests/components/music_assistant/fixtures/players.json index 2d8b88d0e8e..8a08a55dc45 100644 --- a/tests/components/music_assistant/fixtures/players.json +++ b/tests/components/music_assistant/fixtures/players.json @@ -20,7 +20,7 @@ "power", "enqueue" ], - "elapsed_time": 0, + "elapsed_time": null, "elapsed_time_last_updated": 0, "state": "idle", "volume_level": 20, diff --git a/tests/components/music_assistant/snapshots/test_actions.ambr b/tests/components/music_assistant/snapshots/test_actions.ambr new file mode 100644 index 00000000000..6c30ffc512c --- /dev/null +++ b/tests/components/music_assistant/snapshots/test_actions.ambr @@ -0,0 +1,202 @@ +# serializer version: 1 +# name: test_get_library_action + dict({ + 'items': list([ + dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'Traveller', + 'uri': 'library://album/463', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'Chris Stapleton', + 'uri': 'library://artist/433', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Tennessee Whiskey', + 'uri': 'library://track/456', + 'version': '', + }), + dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'Thelma + Louise', + 'uri': 'library://album/471', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'Bastille', + 'uri': 'library://artist/81', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Thelma + Louise', + 'uri': 'library://track/467', + 'version': '', + }), + dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'HIStory - PAST, PRESENT AND FUTURE - BOOK I', + 'uri': 'library://album/486', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'Michael Jackson', + 'uri': 'library://artist/30', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': "They Don't Care About Us", + 'uri': 'library://track/485', + 'version': '', + }), + dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'Better Dayz', + 'uri': 'library://album/487', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': '2Pac', + 'uri': 'library://artist/159', + 'version': '', + }), + dict({ + 'image': None, + 'media_type': , + 'name': 'The Outlawz', + 'uri': 'library://artist/451', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': "They Don't Give A F**** About Us", + 'uri': 'library://track/486', + 'version': '', + }), + dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'Things We Lost In The Fire', + 'uri': 'library://album/488', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'Bastille', + 'uri': 'library://artist/81', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Things We Lost In The Fire', + 'uri': 'library://track/487', + 'version': 'TORN Remix', + }), + dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'Doom Days', + 'uri': 'library://album/489', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'Bastille', + 'uri': 'library://artist/81', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Those Nights', + 'uri': 'library://track/488', + 'version': '', + }), + ]), + 'limit': 25, + 'media_type': , + 'offset': 0, + 'order_by': 'name', + }) +# --- +# name: test_search_action + dict({ + 'albums': list([ + dict({ + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'A Space Love Adventure', + 'uri': 'library://artist/289', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Synth Punk EP', + 'uri': 'library://album/396', + 'version': '', + }), + dict({ + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'Various Artists', + 'uri': 'library://artist/96', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Synthwave (The 80S Revival)', + 'uri': 'library://album/95', + 'version': 'The 80S Revival', + }), + ]), + 'artists': list([ + ]), + 'playlists': list([ + ]), + 'radio': list([ + ]), + 'tracks': list([ + ]), + }) +# --- diff --git a/tests/components/music_assistant/snapshots/test_media_player.ambr b/tests/components/music_assistant/snapshots/test_media_player.ambr index e3d7a4a0cbc..a07bde4b29d 100644 --- a/tests/components/music_assistant/snapshots/test_media_player.ambr +++ b/tests/components/music_assistant/snapshots/test_media_player.ambr @@ -7,6 +7,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -72,6 +73,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -142,6 +144,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -188,3 +191,88 @@ 'state': 'off', }) # --- +# name: test_media_player_get_queue_action + dict({ + 'media_player.test_group_player_1': dict({ + 'active': True, + 'current_index': 26, + 'current_item': dict({ + 'duration': 536, + 'media_item': dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'Use Your Illusion I', + 'uri': 'spotify://album/0CxPbTRARqKUYighiEY9Sz', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': "Guns N' Roses", + 'uri': 'spotify://artist/3qm84nBOXUEQ2vnTfUTTFC', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'November Rain', + 'uri': 'spotify://track/3YRCqOhFifThpSRFJ1VWFM', + 'version': '', + }), + 'name': "Guns N' Roses - November Rain", + 'queue_item_id': '5d95dc5be77e4f7eb4939f62cfef527b', + 'stream_details': dict({ + 'bit_depth': 16, + 'content_type': 'ogg', + 'item_id': '3YRCqOhFifThpSRFJ1VWFM', + 'provider': 'spotify', + 'sample_rate': 44100, + }), + 'stream_title': None, + }), + 'items': 1094, + 'name': 'Test Group Player 1', + 'next_item': dict({ + 'duration': 207, + 'media_item': dict({ + 'album': dict({ + 'image': None, + 'media_type': , + 'name': 'La Folie', + 'uri': 'qobuz://album/0724353468859', + 'version': '', + }), + 'artists': list([ + dict({ + 'image': None, + 'media_type': , + 'name': 'The Stranglers', + 'uri': 'qobuz://artist/26779', + 'version': '', + }), + ]), + 'image': None, + 'media_type': , + 'name': 'Golden Brown', + 'uri': 'qobuz://track/1004735', + 'version': '', + }), + 'name': 'The Stranglers - Golden Brown', + 'queue_item_id': '990ae8f29cdf4fb588d679b115621f55', + 'stream_details': dict({ + 'bit_depth': 16, + 'content_type': 'flac', + 'item_id': '1004735', + 'provider': 'qobuz', + 'sample_rate': 44100, + }), + 'stream_title': None, + }), + 'queue_id': 'test_group_player_1', + 'repeat_mode': 'all', + 'shuffle_enabled': True, + }), + }) +# --- diff --git a/tests/components/music_assistant/test_actions.py b/tests/components/music_assistant/test_actions.py new file mode 100644 index 00000000000..4d3917091c1 --- /dev/null +++ b/tests/components/music_assistant/test_actions.py @@ -0,0 +1,68 @@ +"""Test Music Assistant actions.""" + +from unittest.mock import AsyncMock, MagicMock + +from music_assistant_models.media_items import SearchResults +from syrupy import SnapshotAssertion + +from homeassistant.components.music_assistant.actions import ( + SERVICE_GET_LIBRARY, + SERVICE_SEARCH, +) +from homeassistant.components.music_assistant.const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_FAVORITE, + ATTR_MEDIA_TYPE, + ATTR_SEARCH_NAME, + DOMAIN as MASS_DOMAIN, +) +from homeassistant.core import HomeAssistant + +from .common import create_library_albums_from_fixture, setup_integration_from_fixtures + + +async def test_search_action( + hass: HomeAssistant, + music_assistant_client: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test music assistant search action.""" + entry = await setup_integration_from_fixtures(hass, music_assistant_client) + + music_assistant_client.music.search = AsyncMock( + return_value=SearchResults( + albums=create_library_albums_from_fixture(), + ) + ) + response = await hass.services.async_call( + MASS_DOMAIN, + SERVICE_SEARCH, + { + ATTR_CONFIG_ENTRY_ID: entry.entry_id, + ATTR_SEARCH_NAME: "test", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + + +async def test_get_library_action( + hass: HomeAssistant, + music_assistant_client: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test music assistant get_library action.""" + entry = await setup_integration_from_fixtures(hass, music_assistant_client) + response = await hass.services.async_call( + MASS_DOMAIN, + SERVICE_GET_LIBRARY, + { + ATTR_CONFIG_ENTRY_ID: entry.entry_id, + ATTR_FAVORITE: False, + ATTR_MEDIA_TYPE: "track", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot diff --git a/tests/components/music_assistant/test_config_flow.py b/tests/components/music_assistant/test_config_flow.py index c700060889c..89cda62961b 100644 --- a/tests/components/music_assistant/test_config_flow.py +++ b/tests/components/music_assistant/test_config_flow.py @@ -15,10 +15,10 @@ import pytest from homeassistant.components.music_assistant.config_flow import CONF_URL from homeassistant.components.music_assistant.const import DEFAULT_NAME, DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 13716b6a479..25dfcd22c72 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -6,6 +6,7 @@ from music_assistant_models.enums import MediaType, QueueOption from music_assistant_models.media_items import Track import pytest from syrupy import SnapshotAssertion +from syrupy.filters import paths from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, @@ -32,6 +33,7 @@ from homeassistant.components.music_assistant.media_player import ( ATTR_SOURCE_PLAYER, ATTR_URL, ATTR_USE_PRE_ANNOUNCE, + SERVICE_GET_QUEUE, SERVICE_PLAY_ANNOUNCEMENT, SERVICE_PLAY_MEDIA_ADVANCED, SERVICE_TRANSFER_QUEUE, @@ -583,3 +585,25 @@ async def test_media_player_transfer_queue_action( auto_play=None, require_schema=25, ) + + +async def test_media_player_get_queue_action( + hass: HomeAssistant, + music_assistant_client: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test media_player get_queue action.""" + await setup_integration_from_fixtures(hass, music_assistant_client) + entity_id = "media_player.test_group_player_1" + response = await hass.services.async_call( + MASS_DOMAIN, + SERVICE_GET_QUEUE, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + return_response=True, + ) + # no call is made, this info comes from the cached queue data + assert music_assistant_client.send_command.call_count == 0 + assert response == snapshot(exclude=paths(f"{entity_id}.elapsed_time")) diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index 8ee62996f92..aee6657b270 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -179,4 +179,4 @@ class MyStromSwitchMock(MyStromDeviceMock): """Return the URI.""" if not self._requested_state: return None - return f"http://{self._state["ip"]}" + return f"http://{self._state['ip']}" diff --git a/tests/components/myuplink/fixtures/device-alfred.json b/tests/components/myuplink/fixtures/device-alfred.json new file mode 100644 index 00000000000..ca6f91459f6 --- /dev/null +++ b/tests/components/myuplink/fixtures/device-alfred.json @@ -0,0 +1,40 @@ +{ + "id": "alfred-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "firmware": { + "currentFwVersion": "9682R7A", + "desiredFwVersion": "9682R7A" + }, + "product": { + "serialNumber": "10001", + "name": "Tehowatti Air" + }, + "availableFeatures": { + "settings": true, + "reboot": true, + "forcesync": true, + "forceUpdate": false, + "requestUpdate": false, + "resetAlarm": true, + "triggerEvent": true, + "getMenu": false, + "getMenuChain": false, + "getGuideQuestion": false, + "sendHaystack": true, + "setSmartMode": false, + "setAidMode": true, + "getZones": false, + "processIntent": false, + "boostHotWater": true, + "boostVentilation": true, + "getScheduleConfig": false, + "getScheduleModes": false, + "getScheduleWeekly": false, + "getScheduleVacation": false, + "setScheduleModes": false, + "setScheduleWeekly": false, + "setScheduleOverride": false, + "setScheduleVacation": false, + "setVentilationMode": false + } +} diff --git a/tests/components/myuplink/fixtures/device-batman.json b/tests/components/myuplink/fixtures/device-batman.json new file mode 100644 index 00000000000..f7c079be5dd --- /dev/null +++ b/tests/components/myuplink/fixtures/device-batman.json @@ -0,0 +1,40 @@ +{ + "id": "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "firmware": { + "currentFwVersion": "9682R7B", + "desiredFwVersion": "9682R7B" + }, + "product": { + "serialNumber": "10002", + "name": "F730 CU 3x400V" + }, + "availableFeatures": { + "settings": true, + "reboot": true, + "forcesync": true, + "forceUpdate": false, + "requestUpdate": false, + "resetAlarm": true, + "triggerEvent": true, + "getMenu": false, + "getMenuChain": false, + "getGuideQuestion": false, + "sendHaystack": true, + "setSmartMode": false, + "setAidMode": true, + "getZones": false, + "processIntent": false, + "boostHotWater": true, + "boostVentilation": true, + "getScheduleConfig": false, + "getScheduleModes": false, + "getScheduleWeekly": false, + "getScheduleVacation": false, + "setScheduleModes": false, + "setScheduleWeekly": false, + "setScheduleOverride": false, + "setScheduleVacation": false, + "setVentilationMode": false + } +} diff --git a/tests/components/myuplink/fixtures/device-robin.json b/tests/components/myuplink/fixtures/device-robin.json new file mode 100644 index 00000000000..3155d6e3f70 --- /dev/null +++ b/tests/components/myuplink/fixtures/device-robin.json @@ -0,0 +1,40 @@ +{ + "id": "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "firmware": { + "currentFwVersion": "9682R7C", + "desiredFwVersion": "9682R7C" + }, + "product": { + "serialNumber": "10003", + "name": "SMO 20" + }, + "availableFeatures": { + "settings": true, + "reboot": true, + "forcesync": true, + "forceUpdate": false, + "requestUpdate": false, + "resetAlarm": true, + "triggerEvent": true, + "getMenu": false, + "getMenuChain": false, + "getGuideQuestion": false, + "sendHaystack": true, + "setSmartMode": false, + "setAidMode": true, + "getZones": false, + "processIntent": false, + "boostHotWater": true, + "boostVentilation": true, + "getScheduleConfig": false, + "getScheduleModes": false, + "getScheduleWeekly": false, + "getScheduleVacation": false, + "setScheduleModes": false, + "setScheduleWeekly": false, + "setScheduleOverride": false, + "setScheduleVacation": false, + "setVentilationMode": false + } +} diff --git a/tests/components/myuplink/fixtures/device_points_nibe_f730.json b/tests/components/myuplink/fixtures/device_points_nibe_f730.json index 0a61ab05f21..795a89e7e13 100644 --- a/tests/components/myuplink/fixtures/device_points_nibe_f730.json +++ b/tests/components/myuplink/fixtures/device_points_nibe_f730.json @@ -822,7 +822,7 @@ "parameterUnit": "", "writable": false, "timestamp": "2024-02-08T19:13:05+00:00", - "value": 30, + "value": 31, "strVal": "Heating", "smartHomeCategories": [], "minValue": null, diff --git a/tests/components/myuplink/fixtures/systems-multi.json b/tests/components/myuplink/fixtures/systems-multi.json new file mode 100644 index 00000000000..a587900d23c --- /dev/null +++ b/tests/components/myuplink/fixtures/systems-multi.json @@ -0,0 +1,61 @@ +{ + "page": 1, + "itemsPerPage": 10, + "numItems": 3, + "systems": [ + { + "systemId": "123456-7890-1234", + "name": "Gotham City", + "securityLevel": "admin", + "hasAlarm": false, + "country": "Sweden", + "devices": [ + { + "id": "alfred-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "currentFwVersion": "9682R7A", + "product": { + "serialNumber": "10001", + "name": "Tehowatti Air" + } + } + ] + }, + { + "systemId": "123456-7890-1234", + "name": "Batcave", + "securityLevel": "admin", + "hasAlarm": false, + "country": "Sweden", + "devices": [ + { + "id": "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "currentFwVersion": "9682R7B", + "product": { + "serialNumber": "10002", + "name": "F730 CU 3x400V" + } + } + ] + }, + { + "systemId": "123456-7890-1234", + "name": "Duckburg", + "securityLevel": "admin", + "hasAlarm": false, + "country": "Sweden", + "devices": [ + { + "id": "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "currentFwVersion": "9682R7C", + "product": { + "serialNumber": "10003", + "name": "SM0 20" + } + } + ] + } + ] +} diff --git a/tests/components/myuplink/snapshots/test_binary_sensor.ambr b/tests/components/myuplink/snapshots/test_binary_sensor.ambr index 755cae3c623..478c5a55b80 100644 --- a/tests/components/myuplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/myuplink/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -193,6 +197,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -239,6 +244,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -285,6 +291,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/myuplink/snapshots/test_diagnostics.ambr b/tests/components/myuplink/snapshots/test_diagnostics.ambr index 6fe6becff11..521823e282d 100644 --- a/tests/components/myuplink/snapshots/test_diagnostics.ambr +++ b/tests/components/myuplink/snapshots/test_diagnostics.ambr @@ -883,7 +883,7 @@ "parameterUnit": "", "writable": false, "timestamp": "2024-02-08T19:13:05+00:00", - "value": 30, + "value": 31, "strVal": "Heating", "smartHomeCategories": [], "minValue": null, @@ -2045,7 +2045,7 @@ "parameterUnit": "", "writable": false, "timestamp": "2024-02-08T19:13:05+00:00", - "value": 30, + "value": 31, "strVal": "Heating", "smartHomeCategories": [], "minValue": null, diff --git a/tests/components/myuplink/snapshots/test_init.ambr b/tests/components/myuplink/snapshots/test_init.ambr new file mode 100644 index 00000000000..14be11c36ec --- /dev/null +++ b/tests/components/myuplink/snapshots/test_init.ambr @@ -0,0 +1,100 @@ +# serializer version: 1 +# name: test_device_info[alfred-multi] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'myuplink', + 'alfred-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Jäspi', + 'model': 'Tehowatti Air', + 'model_id': None, + 'name': 'Gotham City', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '10001', + 'suggested_area': None, + 'sw_version': '9682R7A', + 'via_device_id': None, + }) +# --- +# name: test_device_info[batman-multi] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'myuplink', + 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Nibe', + 'model': 'F730', + 'model_id': None, + 'name': 'Batcave', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '10002', + 'suggested_area': None, + 'sw_version': '9682R7B', + 'via_device_id': None, + }) +# --- +# name: test_device_info[robin-multi] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'myuplink', + 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Nibe', + 'model': 'SMO 20', + 'model_id': None, + 'name': 'Duckburg', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '10003', + 'suggested_area': None, + 'sw_version': '9682R7C', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/myuplink/snapshots/test_number.ambr b/tests/components/myuplink/snapshots/test_number.ambr index c47d3c60295..f2c89663879 100644 --- a/tests/components/myuplink/snapshots/test_number.ambr +++ b/tests/components/myuplink/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -123,6 +125,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -178,6 +181,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -233,6 +237,7 @@ 'step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +293,7 @@ 'step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -343,6 +349,7 @@ 'step': 10.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -399,6 +406,7 @@ 'step': 10.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/myuplink/snapshots/test_select.ambr b/tests/components/myuplink/snapshots/test_select.ambr index eff06bc7f2d..032fd2ef455 100644 --- a/tests/components/myuplink/snapshots/test_select.ambr +++ b/tests/components/myuplink/snapshots/test_select.ambr @@ -13,6 +13,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -72,6 +73,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/myuplink/snapshots/test_sensor.ambr b/tests/components/myuplink/snapshots/test_sensor.ambr index a5469dc9a77..f9249651208 100644 --- a/tests/components/myuplink/snapshots/test_sensor.ambr +++ b/tests/components/myuplink/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +164,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -212,6 +216,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -263,6 +268,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -314,6 +320,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -365,6 +372,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -416,6 +424,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -467,6 +476,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -518,6 +528,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -569,6 +580,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -620,6 +632,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -671,6 +684,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -720,6 +734,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -766,6 +781,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -812,6 +828,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -859,6 +876,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -908,6 +926,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -959,6 +978,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1008,6 +1028,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1055,6 +1076,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1104,6 +1126,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1158,6 +1181,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1210,6 +1234,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1257,6 +1282,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1304,6 +1330,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1351,6 +1378,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1398,6 +1426,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1445,6 +1474,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1494,6 +1524,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1545,6 +1576,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1596,6 +1628,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1647,6 +1680,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1698,6 +1732,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1749,6 +1784,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1800,6 +1836,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1851,6 +1888,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1902,6 +1940,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1953,6 +1992,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2002,6 +2042,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2049,6 +2090,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2098,6 +2140,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2149,6 +2192,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2200,6 +2244,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2251,6 +2296,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2302,6 +2348,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2353,6 +2400,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2404,6 +2452,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2455,6 +2504,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2514,6 +2564,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2580,6 +2631,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2636,6 +2688,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2682,6 +2735,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2730,6 +2784,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2781,6 +2836,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2832,6 +2888,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2883,6 +2940,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2934,6 +2992,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2985,6 +3044,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3036,6 +3096,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3087,6 +3148,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3138,6 +3200,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3189,6 +3252,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3240,6 +3304,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3291,6 +3356,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3350,6 +3416,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3396,7 +3463,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Heating', + 'state': 'unknown', }) # --- # name: test_sensor_states[sensor.gotham_city_priority_2-entry] @@ -3416,6 +3483,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3462,7 +3530,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Heating', + 'state': 'unknown', }) # --- # name: test_sensor_states[sensor.gotham_city_priority_raw-entry] @@ -3472,6 +3540,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3508,7 +3577,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '30', + 'state': '31', }) # --- # name: test_sensor_states[sensor.gotham_city_priority_raw_2-entry] @@ -3518,6 +3587,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3554,7 +3624,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '30', + 'state': '31', }) # --- # name: test_sensor_states[sensor.gotham_city_r_start_diff_additional_heat-entry] @@ -3564,6 +3634,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3611,6 +3682,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3660,6 +3732,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3711,6 +3784,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3762,6 +3836,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3813,6 +3888,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3864,6 +3940,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3915,6 +3992,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3966,6 +4044,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4017,6 +4096,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4073,6 +4153,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4133,6 +4214,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4186,6 +4268,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4232,6 +4315,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4280,6 +4364,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4331,6 +4416,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4382,6 +4468,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4433,6 +4520,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4484,6 +4572,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4535,6 +4624,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4584,6 +4674,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4631,6 +4722,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4678,6 +4770,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4725,6 +4818,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/myuplink/snapshots/test_switch.ambr b/tests/components/myuplink/snapshots/test_switch.ambr index 5d621e661ee..142d4caa455 100644 --- a/tests/components/myuplink/snapshots/test_switch.ambr +++ b/tests/components/myuplink/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/myuplink/test_init.py b/tests/components/myuplink/test_init.py index fda0d3526f9..320bf202024 100644 --- a/tests/components/myuplink/test_init.py +++ b/tests/components/myuplink/test_init.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock from aiohttp import ClientConnectionError import pytest +from syrupy import SnapshotAssertion from homeassistant.components.myuplink.const import DOMAIN, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntryState @@ -214,3 +215,47 @@ async def test_device_remove_devices( old_device_entry.id, mock_config_entry.entry_id ) assert response["success"] + + +@pytest.mark.parametrize( + "load_systems_file", + [load_fixture("systems-multi.json", DOMAIN)], + ids=[ + "multi", + ], +) +@pytest.mark.parametrize( + ("load_device_file", "device_id"), + [ + ( + load_fixture("device-alfred.json", DOMAIN), + "alfred-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + ), + ( + load_fixture("device-batman.json", DOMAIN), + "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + ), + ( + load_fixture("device-robin.json", DOMAIN), + "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + ), + ], + ids=[ + "alfred", + "batman", + "robin", + ], +) +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_myuplink_client: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + device_id: str, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + assert device_entry is not None + assert device_entry == snapshot diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index 16129c5d7ce..429d069b741 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -62,6 +63,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -116,6 +118,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -170,6 +173,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -224,6 +228,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -278,6 +283,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -332,6 +338,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -386,6 +393,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -440,6 +448,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -494,6 +503,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -548,6 +558,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -602,6 +613,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -654,6 +666,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -703,6 +716,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -755,6 +769,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -809,6 +824,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -865,6 +881,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -919,6 +936,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -973,6 +991,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1025,6 +1044,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1079,6 +1099,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1135,6 +1156,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1189,6 +1211,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1243,6 +1266,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1297,6 +1321,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1351,6 +1376,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1403,6 +1429,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1457,6 +1484,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1513,6 +1541,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1567,6 +1596,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1621,6 +1651,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1675,6 +1706,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 6c11399c888..80c6e86f420 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -6,16 +6,16 @@ from unittest.mock import patch from nettigo_air_monitor import ApiError, AuthFailedError, CannotGetMacError import pytest -from homeassistant.components import zeroconf from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=ip_address("10.10.2.3"), ip_addresses=[ip_address("10.10.2.3")], hostname="mock_hostname", diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 97a314b0bf4..ba89405bc97 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -9,11 +9,15 @@ from aionanoleaf import InvalidToken, Unauthorized, Unavailable import pytest from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf from homeassistant.components.nanoleaf.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from tests.common import MockConfigEntry @@ -248,13 +252,13 @@ async def test_discovery_link_unavailable( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", name=f"{TEST_NAME}.{type_in_discovery_info}", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, + properties={ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, type=type_in_discovery_info, ), ) @@ -384,13 +388,13 @@ async def test_import_discovery_integration( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", name=f"{TEST_NAME}.{type_in_discovery}", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, + properties={ATTR_PROPERTIES_ID: TEST_DEVICE_ID}, type=type_in_discovery, ), ) @@ -432,7 +436,7 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={}, diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 8f1f0a2f074..803f6076ecc 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -5,17 +5,14 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator import copy from dataclasses import dataclass +import re from typing import Any -from google_nest_sdm.auth import AbstractAuth -from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.event import EventMessage -from google_nest_sdm.event_media import CachePolicy -from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from google_nest_sdm.streaming_manager import Message from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import API_URL # Typing helpers type PlatformSetup = Callable[[], Awaitable[None]] @@ -66,68 +63,22 @@ TEST_CONFIG_NEW_SUBSCRIPTION = NestTestConfig( credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), ) - -class FakeSubscriber(GoogleNestSubscriber): - """Fake subscriber that supplies a FakeDeviceManager.""" - - stop_calls = 0 - - def __init__(self) -> None: # pylint: disable=super-init-not-called - """Initialize Fake Subscriber.""" - self._device_manager = DeviceManager() - self._subscriber_name = "fake-name" - - def set_update_callback(self, target: Callable[[EventMessage], Awaitable[None]]): - """Capture the callback set by Home Assistant.""" - self._device_manager.set_update_callback(target) - - async def create_subscription(self): - """Create the subscription.""" - return - - async def delete_subscription(self): - """Delete the subscription.""" - return - - async def start_async(self): - """Return the fake device manager.""" - return self._device_manager - - async def async_get_device_manager(self) -> DeviceManager: - """Return the fake device manager.""" - return self._device_manager - - @property - def cache_policy(self) -> CachePolicy: - """Return the cache policy.""" - return self._device_manager.cache_policy - - def stop_async(self): - """No-op to stop the subscriber.""" - self.stop_calls += 1 - - async def async_receive_event(self, event_message: EventMessage): - """Simulate a received pubsub message, invoked by tests.""" - # Update device state, then invoke HomeAssistant to refresh - await self._device_manager.async_handle_event(event_message) - - -DEVICE_ID = "enterprise/project-id/devices/device-id" +DEVICE_ID = "enterprises/project-id/devices/device-id" DEVICE_COMMAND = f"{DEVICE_ID}:executeCommand" +DEVICE_URL_MATCH = re.compile( + f"{API_URL}/enterprises/project-id/devices/[^:]+:executeCommand" +) +TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." +TEST_CLIP_URL = "https://domain/clip/XyZ.mp4" class CreateDevice: """Fixture used for creating devices.""" - def __init__( - self, - device_manager: DeviceManager, - auth: AbstractAuth, - ) -> None: + def __init__(self) -> None: """Initialize CreateDevice.""" - self.device_manager = device_manager - self.auth = auth self.data = {"traits": {}} + self.devices = [] def create( self, @@ -138,4 +89,9 @@ class CreateDevice: data = copy.deepcopy(self.data) data.update(raw_data if raw_data else {}) data["traits"].update(raw_traits if raw_traits else {}) - self.device_manager.add_device(Device.MakeDevice(data, auth=self.auth)) + self.devices.append(data) + + +def create_nest_event(data: dict[str, Any]) -> Message: + """Create a pub/sub event message for testing.""" + return Message.from_data(data) diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 84f22e17e78..92d90a18a7e 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations -from asyncio import AbstractEventLoop from collections.abc import Generator import copy import shutil @@ -13,100 +12,136 @@ import uuid import aiohttp from google_nest_sdm import diagnostics -from google_nest_sdm.auth import AbstractAuth -from google_nest_sdm.device_manager import DeviceManager +from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from google_nest_sdm.streaming_manager import StreamingManager import pytest +from yarl import URL from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.nest import DOMAIN -from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID, SDM_SCOPES +from homeassistant.components.nest.const import API_URL, CONF_SUBSCRIBER_ID, SDM_SCOPES from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .common import ( DEVICE_ID, + DEVICE_URL_MATCH, PROJECT_ID, SUBSCRIBER_ID, + TEST_CLIP_URL, TEST_CONFIG_APP_CREDS, + TEST_IMAGE_URL, CreateDevice, - FakeSubscriber, NestTestConfig, PlatformSetup, YieldFixture, ) from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse FAKE_TOKEN = "some-token" FAKE_REFRESH_TOKEN = "some-refresh-token" -class FakeAuth(AbstractAuth): - """A fake implementation of the auth class that records requests. +class FakeAuth: + """A fixture for request handling that records requests. - This class captures the outgoing requests, and can also be used by - tests to set up fake responses. This class is registered as a response - handler for a fake aiohttp_server and can simulate successes or failures - from the API. + This class is used with AiohttpClientMocker to capture outgoing requests + and can also be used by tests to set up fake responses. """ - def __init__(self) -> None: + def __init__( + self, + aioclient_mock: AiohttpClientMocker, + device_factory: CreateDevice, + project_id: str, + ) -> None: """Initialize FakeAuth.""" - super().__init__(None, None) - # Tests can set fake responses here. - self.responses = [] + # Tests can factory fixture to create fake device responses. + self.device_factory = device_factory + # Tests can set fake structure responses here. + self.structures: list[dict[str, Any]] = [] + # Tests can set fake command responses here. + self.responses: list[aiohttp.web.Response] = [] + # The last request is recorded here. self.method = None self.url = None self.json = None self.headers = None self.captured_requests = [] - # Set up by fixture - self.client = None + self._project_id = project_id + self._aioclient_mock = aioclient_mock + self.register_mock_requests() - async def async_get_access_token(self) -> str: - """Return a valid access token.""" - return "" + def register_mock_requests(self) -> None: + """Register the mocks.""" + # API makes a call to request structures to initiate pubsub feed, but the + # integration does not use this. + self._aioclient_mock.get( + f"{API_URL}/enterprises/{self._project_id}/structures", + side_effect=self.request_structures, + ) + self._aioclient_mock.get( + f"{API_URL}/enterprises/{self._project_id}/devices", + side_effect=self.request_devices, + ) + self._aioclient_mock.post(DEVICE_URL_MATCH, side_effect=self.request) + self._aioclient_mock.get(TEST_IMAGE_URL, side_effect=self.request) + self._aioclient_mock.get(TEST_CLIP_URL, side_effect=self.request) - async def request(self, method, url, **kwargs): + async def request_structures( + self, method: str, url: str, data: dict[str, Any] + ) -> AiohttpClientMockResponse: + """Handle requests to create devices.""" + return AiohttpClientMockResponse( + method, url, json={"structures": self.structures} + ) + + async def request_devices( + self, method: str, url: str, data: dict[str, Any] + ) -> AiohttpClientMockResponse: + """Handle requests to create devices.""" + return AiohttpClientMockResponse( + method, url, json={"devices": self.device_factory.devices} + ) + + async def request( + self, method: str, url: URL, data: dict[str, Any] + ) -> AiohttpClientMockResponse: """Capure the request arguments for tests to assert on.""" self.method = method - self.url = url - self.json = kwargs.get("json") - self.headers = kwargs.get("headers") - self.captured_requests.append((method, url, self.json, self.headers)) - return await self.client.get("/") + str_url = str(url) + self.url = str_url[len(API_URL) + 1 :] + self.json = data + self.captured_requests.append((method, url, self.json)) - async def response_handler(self, request): - """Handle fake responess for aiohttp_server.""" if len(self.responses) > 0: - return self.responses.pop(0) - return aiohttp.web.json_response() + response = self.responses.pop(0) + return AiohttpClientMockResponse( + method, url, response=response.body, status=response.status + ) + return AiohttpClientMockResponse(method, url) + + +@pytest.fixture(name="device_access_project_id") +def mock_device_access_project_id() -> str: + """Fixture to configure the device access console project id used in tests.""" + return PROJECT_ID @pytest.fixture -def aiohttp_client( - event_loop: AbstractEventLoop, - aiohttp_client: ClientSessionGenerator, - socket_enabled: None, -) -> ClientSessionGenerator: - """Return aiohttp_client and allow opening sockets.""" - return aiohttp_client - - -@pytest.fixture -async def auth(aiohttp_client: ClientSessionGenerator) -> FakeAuth: +async def auth( + aioclient_mock: AiohttpClientMocker, + create_device: CreateDevice, + device_access_project_id: str, +) -> FakeAuth: """Fixture for an AbstractAuth.""" - auth = FakeAuth() - app = aiohttp.web.Application() - app.router.add_get("/", auth.response_handler) - app.router.add_post("/", auth.response_handler) - auth.client = await aiohttp_client(app) - return auth + return FakeAuth(aioclient_mock, create_device, device_access_project_id) @pytest.fixture(autouse=True) @@ -119,20 +154,32 @@ def cleanup_media_storage(hass: HomeAssistant) -> Generator[None]: @pytest.fixture -def subscriber() -> YieldFixture[FakeSubscriber]: - """Set up the FakeSusbcriber.""" - subscriber = FakeSubscriber() +def subscriber_side_effect() -> Any | None: + """Fixture to inject failures into FakeSubscriber start.""" + return None + + +@pytest.fixture(autouse=True) +def subscriber(subscriber_side_effect: Any | None) -> Generator[AsyncMock]: + """Fixture to allow tests to emulate the pub/sub subscriber receiving messages.""" with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - yield subscriber + "google_nest_sdm.google_nest_subscriber.StreamingManager", spec=StreamingManager + ) as mock_manager: + # Use side_effect to capture the callback + def mock_init(**kwargs: Any) -> AsyncMock: + mock_manager.async_receive_event = kwargs["callback"] + if subscriber_side_effect is not None: + mock_manager.start.side_effect = subscriber_side_effect + return mock_manager + + mock_manager.side_effect = mock_init + yield mock_manager @pytest.fixture def mock_subscriber() -> YieldFixture[AsyncMock]: """Fixture for injecting errors into the subscriber.""" - mock_subscriber = AsyncMock(FakeSubscriber) + mock_subscriber = AsyncMock(GoogleNestSubscriber) with patch( "homeassistant.components.nest.api.GoogleNestSubscriber", return_value=mock_subscriber, @@ -140,12 +187,6 @@ def mock_subscriber() -> YieldFixture[AsyncMock]: yield mock_subscriber -@pytest.fixture -async def device_manager(subscriber: FakeSubscriber) -> DeviceManager: - """Set up the DeviceManager.""" - return await subscriber.async_get_device_manager() - - @pytest.fixture async def device_id() -> str: """Fixture to set default device id used when creating devices.""" @@ -166,14 +207,12 @@ async def device_traits() -> dict[str, Any]: @pytest.fixture async def create_device( - device_manager: DeviceManager, - auth: FakeAuth, device_id: str, device_type: str, device_traits: dict[str, Any], ) -> CreateDevice: """Fixture for creating devices.""" - factory = CreateDevice(device_manager, auth) + factory = CreateDevice() factory.data.update( { "name": device_id, @@ -262,6 +301,7 @@ async def setup_base_platform( hass: HomeAssistant, platforms: list[str], config_entry: MockConfigEntry | None, + auth: FakeAuth, ) -> YieldFixture[PlatformSetup]: """Fixture to setup the integration platform.""" config_entry.add_to_hass(hass) @@ -278,7 +318,8 @@ async def setup_base_platform( @pytest.fixture async def setup_platform( - setup_base_platform: PlatformSetup, subscriber: FakeSubscriber + setup_base_platform: PlatformSetup, + subscriber: AsyncMock, ) -> PlatformSetup: """Fixture to setup the integration platform and subscriber.""" return setup_base_platform diff --git a/tests/components/nest/snapshots/test_diagnostics.ambr b/tests/components/nest/snapshots/test_diagnostics.ambr index aa679b8821c..5e17156fd6a 100644 --- a/tests/components/nest/snapshots/test_diagnostics.ambr +++ b/tests/components/nest/snapshots/test_diagnostics.ambr @@ -31,6 +31,9 @@ }), }), ]), + 'subscriber': dict({ + 'start': 1, + }), }) # --- # name: test_device_diagnostics @@ -85,5 +88,8 @@ }), }), ]), + 'subscriber': dict({ + 'start': 1, + }), }) # --- diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index a13d4d3a337..1a5c4d63dba 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -9,16 +9,16 @@ The tests below exercise both cases during integration setup. """ import time -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch -from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from google.oauth2.credentials import Credentials import pytest from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, FakeSubscriber, PlatformSetup +from .common import CLIENT_ID, CLIENT_SECRET, PROJECT_ID, PlatformSetup from .conftest import FAKE_REFRESH_TOKEN, FAKE_TOKEN from tests.test_util.aiohttp import AiohttpClientMocker @@ -27,7 +27,7 @@ FAKE_UPDATED_TOKEN = "fake-updated-token" @pytest.fixture -def subscriber() -> FakeSubscriber | None: +def subscriber() -> Mock | None: """Disable default subscriber since tests use their own patch.""" return None @@ -54,16 +54,16 @@ async def test_auth( # Prepare to capture credentials for Subscriber captured_creds = None - async def async_new_subscriber( - creds, subscription_name, event_loop, async_callback - ) -> GoogleNestSubscriber | None: + def async_new_subscriber( + credentials: Credentials, + ) -> Mock: """Capture credentials for tests.""" nonlocal captured_creds - captured_creds = creds - return None # GoogleNestSubscriber + captured_creds = credentials + return AsyncMock() with patch( - "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + "google_nest_sdm.subscriber_client.pubsub_v1.SubscriberAsyncClient", side_effect=async_new_subscriber, ) as new_subscriber_mock: await setup_platform() @@ -89,80 +89,3 @@ async def test_auth( assert creds.client_id == CLIENT_ID assert creds.client_secret == CLIENT_SECRET assert creds.scopes == SDM_SCOPES - - -# This tests needs to be adjusted to remove lingering tasks -@pytest.mark.parametrize("expected_lingering_tasks", [True]) -@pytest.mark.parametrize( - "token_expiration_time", - [time.time() - 7 * 86400], - ids=["expires-in-past"], -) -async def test_auth_expired_token( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - setup_platform: PlatformSetup, - token_expiration_time: float, -) -> None: - """Verify behavior of an expired token.""" - # Prepare a token refresh response - aioclient_mock.post( - OAUTH2_TOKEN, - json={ - "access_token": FAKE_UPDATED_TOKEN, - "expires_at": time.time() + 86400, - "expires_in": 86400, - }, - ) - # Prepare to capture credentials in API request. Empty payloads just mean - # no devices or structures are loaded. - aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={}) - aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/devices", json={}) - - # Prepare to capture credentials for Subscriber - captured_creds = None - - async def async_new_subscriber( - creds, subscription_name, event_loop, async_callback - ) -> GoogleNestSubscriber | None: - """Capture credentials for tests.""" - nonlocal captured_creds - captured_creds = creds - return None # GoogleNestSubscriber - - with patch( - "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", - side_effect=async_new_subscriber, - ) as new_subscriber_mock: - await setup_platform() - - calls = aioclient_mock.mock_calls - assert len(calls) == 3 - # Verify refresh token call to get an updated token - (method, url, data, headers) = calls[0] - assert data == { - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - "grant_type": "refresh_token", - "refresh_token": FAKE_REFRESH_TOKEN, - } - # Verify API requests are made with the new token - (method, url, data, headers) = calls[1] - assert headers == {"Authorization": f"Bearer {FAKE_UPDATED_TOKEN}"} - (method, url, data, headers) = calls[2] - assert headers == {"Authorization": f"Bearer {FAKE_UPDATED_TOKEN}"} - - # The subscriber is created with a token that is expired. Verify that the - # credential is expired so the subscriber knows it needs to refresh it. - assert len(new_subscriber_mock.mock_calls) == 1 - assert captured_creds - creds = captured_creds - assert creds.token == FAKE_TOKEN - assert creds.refresh_token == FAKE_REFRESH_TOKEN - assert int(dt_util.as_timestamp(creds.expiry)) == int(token_expiration_time) - assert not creds.valid - assert creds.expired - assert creds.token_uri == OAUTH2_TOKEN - assert creds.client_id == CLIENT_ID - assert creds.client_secret == CLIENT_SECRET - assert creds.scopes == SDM_SCOPES diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 698e9b7a274..3e7dbd3f223 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -24,7 +24,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .common import DEVICE_ID, CreateDevice, PlatformSetup from .conftest import FakeAuth from tests.common import async_fire_time_changed @@ -520,13 +520,10 @@ async def test_camera_removed( hass: HomeAssistant, auth: FakeAuth, camera_device: None, - subscriber: FakeSubscriber, setup_platform: PlatformSetup, ) -> None: """Test case where entities are removed and stream tokens revoked.""" await setup_platform() - # Simplify test setup - subscriber.cache_policy.fetch = False assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -780,7 +777,7 @@ async def test_camera_web_rtc_offer_failure( assert response["event"] == { "type": "error", "code": "webrtc_offer_failed", - "message": "Nest API error: Bad Request response from API (400)", + "message": "Nest API error: response from API (400)", } diff --git a/tests/components/nest/test_climate.py b/tests/components/nest/test_climate.py index 88847759a16..fe148c2529d 100644 --- a/tests/components/nest/test_climate.py +++ b/tests/components/nest/test_climate.py @@ -7,13 +7,13 @@ pubsub subscriber. from collections.abc import Awaitable, Callable from http import HTTPStatus from typing import Any +from unittest.mock import AsyncMock import aiohttp -from google_nest_sdm.auth import AbstractAuth -from google_nest_sdm.event import EventMessage import pytest from homeassistant.components.climate import ( + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_FAN_MODES, @@ -45,8 +45,8 @@ from .common import ( DEVICE_COMMAND, DEVICE_ID, CreateDevice, - FakeSubscriber, PlatformSetup, + create_nest_event, ) from .conftest import FakeAuth @@ -72,14 +72,13 @@ def device_traits() -> dict[str, Any]: @pytest.fixture async def create_event( hass: HomeAssistant, - auth: AbstractAuth, - subscriber: FakeSubscriber, + subscriber: AsyncMock, ) -> CreateEvent: """Fixture to send a pub/sub event.""" async def create_event(traits: dict[str, Any]) -> None: await subscriber.async_receive_event( - EventMessage.create_event( + create_nest_event( { "eventId": EVENT_ID, "timestamp": "2019-01-01T00:00:01Z", @@ -88,7 +87,6 @@ async def create_event( "traits": traits, }, }, - auth=auth, ) ) await hass.async_block_till_done() @@ -125,6 +123,9 @@ async def test_thermostat_off( "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 40.6, + }, }, ) await setup_platform() @@ -134,6 +135,7 @@ async def test_thermostat_off( assert thermostat is not None assert thermostat.state == HVACMode.OFF assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF + assert thermostat.attributes[ATTR_CURRENT_HUMIDITY] == 40.6 assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVACMode.HEAT, @@ -166,6 +168,9 @@ async def test_thermostat_heat( "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 40.6, + }, "sdm.devices.traits.ThermostatTemperatureSetpoint": { "heatCelsius": 22.0, }, @@ -178,6 +183,7 @@ async def test_thermostat_heat( assert thermostat is not None assert thermostat.state == HVACMode.HEAT assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING + assert thermostat.attributes[ATTR_CURRENT_HUMIDITY] == 40.6 assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVACMode.HEAT, @@ -886,6 +892,9 @@ async def test_thermostat_fan_off( "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 40.6, + }, } ) await setup_platform() @@ -895,6 +904,7 @@ async def test_thermostat_fan_off( assert thermostat is not None assert thermostat.state == HVACMode.COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE + assert thermostat.attributes[ATTR_CURRENT_HUMIDITY] == 40.6 assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVACMode.HEAT, @@ -935,6 +945,9 @@ async def test_thermostat_fan_on( "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 40.6, + }, } ) await setup_platform() @@ -944,6 +957,7 @@ async def test_thermostat_fan_on( assert thermostat is not None assert thermostat.state == HVACMode.COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE + assert thermostat.attributes[ATTR_CURRENT_HUMIDITY] == 40.6 assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVACMode.HEAT, @@ -1131,6 +1145,9 @@ async def test_thermostat_fan_empty( "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 40.6, + }, } ) await setup_platform() @@ -1140,6 +1157,7 @@ async def test_thermostat_fan_empty( assert thermostat is not None assert thermostat.state == HVACMode.OFF assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF + assert thermostat.attributes[ATTR_CURRENT_HUMIDITY] == 40.6 assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVACMode.HEAT, @@ -1184,6 +1202,9 @@ async def test_thermostat_invalid_fan_mode( "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, }, + "sdm.devices.traits.Humidity": { + "ambientHumidityPercent": 40.6, + }, } ) await setup_platform() @@ -1193,6 +1214,7 @@ async def test_thermostat_invalid_fan_mode( assert thermostat is not None assert thermostat.state == HVACMode.COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE + assert thermostat.attributes[ATTR_CURRENT_HUMIDITY] == 40.6 assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVACMode.HEAT, diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 807e299b79c..0e6ec290841 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -7,16 +7,15 @@ from typing import Any from unittest.mock import patch from google_nest_sdm.exceptions import AuthException -from google_nest_sdm.structure import Structure import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .common import ( CLIENT_ID, @@ -25,9 +24,9 @@ from .common import ( SUBSCRIBER_ID, TEST_CONFIG_APP_CREDS, TEST_CONFIGFLOW_APP_CREDS, - FakeSubscriber, NestTestConfig, ) +from .conftest import FakeAuth, PlatformSetup from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -35,9 +34,9 @@ from tests.typing import ClientSessionGenerator WEB_REDIRECT_URL = "https://example.com/auth/external/callback" APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" -RAND_SUBSCRIBER_SUFFIX = "ABCDEF" +RAND_SUFFIX = "ABCDEF" -FAKE_DHCP_DATA = dhcp.DhcpServiceInfo( +FAKE_DHCP_DATA = DhcpServiceInfo( ip="127.0.0.2", macaddress="001122334455", hostname="fake_hostname" ) @@ -53,11 +52,16 @@ def mock_rand_topic_name_fixture() -> None: """Set the topic name random string to a constant.""" with patch( "homeassistant.components.nest.config_flow.get_random_string", - return_value=RAND_SUBSCRIBER_SUFFIX, + return_value=RAND_SUFFIX, ): yield +@pytest.fixture(autouse=True) +def mock_request_setup(auth: FakeAuth) -> None: + """Fixture to ensure fake requests are setup.""" + + class OAuthFixture: """Simulate the oauth flow used by the config flow.""" @@ -169,6 +173,7 @@ class OAuthFixture: selected_topic: str, selected_subscription: str = "create_new_subscription", user_input: dict | None = None, + existing_errors: dict | None = None, ) -> ConfigEntry: """Fixture to walk through the Pub/Sub topic and subscription steps. @@ -189,6 +194,12 @@ class OAuthFixture: }, ) assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic_confirm" + assert not result.get("errors") + + # ACK the topic selection. User is instructed to do some manual + result = await self.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pubsub_subscription" assert not result.get("errors") @@ -257,18 +268,18 @@ def mock_subscriptions() -> list[tuple[str, str]]: return [] -@pytest.fixture(name="device_access_project_id") -def mock_device_access_project_id() -> str: - """Fixture to configure the device access console project id used in tests.""" - return PROJECT_ID - - @pytest.fixture(name="cloud_project_id") def mock_cloud_project_id() -> str: """Fixture to configure the cloud console project id used in tests.""" return CLOUD_PROJECT_ID +@pytest.fixture(name="create_topic_status") +def mock_create_topic_status() -> str: + """Fixture to configure the return code when creating the topic.""" + return HTTPStatus.OK + + @pytest.fixture(name="create_subscription_status") def mock_create_subscription_status() -> str: """Fixture to configure the return code when creating the subscription.""" @@ -287,6 +298,64 @@ def mock_list_subscriptions_status() -> str: return HTTPStatus.OK +def setup_mock_list_subscriptions_responses( + aioclient_mock: AiohttpClientMocker, + cloud_project_id: str, + subscriptions: list[tuple[str, str]], + list_subscriptions_status: HTTPStatus = HTTPStatus.OK, +) -> None: + """Configure the mock responses for listing Pub/Sub subscriptions.""" + aioclient_mock.get( + f"https://pubsub.googleapis.com/v1/projects/{cloud_project_id}/subscriptions", + json={ + "subscriptions": [ + { + "name": subscription_name, + "topic": topic, + "pushConfig": {}, + "ackDeadlineSeconds": 10, + "messageRetentionDuration": "604800s", + "expirationPolicy": {"ttl": "2678400s"}, + "state": "ACTIVE", + } + for (subscription_name, topic) in subscriptions or () + ] + }, + status=list_subscriptions_status, + ) + + +def setup_mock_create_topic_responses( + aioclient_mock: AiohttpClientMocker, + cloud_project_id: str, + create_topic_status: HTTPStatus = HTTPStatus.OK, +) -> None: + """Configure the mock responses for creating a Pub/Sub topic.""" + aioclient_mock.put( + f"https://pubsub.googleapis.com/v1/projects/{cloud_project_id}/topics/home-assistant-{RAND_SUFFIX}", + json={}, + status=create_topic_status, + ) + aioclient_mock.post( + f"https://pubsub.googleapis.com/v1/projects/{cloud_project_id}/topics/home-assistant-{RAND_SUFFIX}:setIamPolicy", + json={}, + status=create_topic_status, + ) + + +def setup_mock_create_subscription_responses( + aioclient_mock: AiohttpClientMocker, + cloud_project_id: str, + create_subscription_status: HTTPStatus = HTTPStatus.OK, +) -> None: + """Configure the mock responses for creating a Pub/Sub subscription.""" + aioclient_mock.put( + f"https://pubsub.googleapis.com/v1/projects/{cloud_project_id}/subscriptions/home-assistant-{RAND_SUFFIX}", + json={}, + status=create_subscription_status, + ) + + @pytest.fixture(autouse=True) def mock_pubsub_api_responses( aioclient_mock: AiohttpClientMocker, @@ -295,6 +364,7 @@ def mock_pubsub_api_responses( subscriptions: list[tuple[str, str]], device_access_project_id: str, cloud_project_id: str, + create_topic_status: HTTPStatus, create_subscription_status: HTTPStatus, list_topics_status: HTTPStatus, list_subscriptions_status: HTTPStatus, @@ -322,36 +392,21 @@ def mock_pubsub_api_responses( ) # We check for a topic created by the SDM Device Access Console (but note we don't have permission to read it) # or the user has created one themselves in the Google Cloud Project. - aioclient_mock.get( - f"https://pubsub.googleapis.com/v1/projects/{cloud_project_id}/subscriptions", - json={ - "subscriptions": [ - { - "name": subscription_name, - "topic": topic, - "pushConfig": {}, - "ackDeadlineSeconds": 10, - "messageRetentionDuration": "604800s", - "expirationPolicy": {"ttl": "2678400s"}, - "state": "ACTIVE", - } - for (subscription_name, topic) in subscriptions or () - ] - }, - status=list_subscriptions_status, + setup_mock_list_subscriptions_responses( + aioclient_mock, cloud_project_id, subscriptions, list_subscriptions_status ) - aioclient_mock.put( - f"https://pubsub.googleapis.com/v1/projects/{cloud_project_id}/subscriptions/home-assistant-{RAND_SUBSCRIBER_SUFFIX}", - json={}, - status=create_subscription_status, + setup_mock_create_topic_responses( + aioclient_mock, cloud_project_id, create_topic_status + ) + setup_mock_create_subscription_responses( + aioclient_mock, cloud_project_id, create_subscription_status ) @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_app_credentials( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( @@ -374,7 +429,7 @@ async def test_app_credentials( "auth_implementation": "imported-cred", "cloud_project_id": CLOUD_PROJECT_ID, "project_id": PROJECT_ID, - "subscription_name": f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUBSCRIBER_SUFFIX}", + "subscription_name": f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUFFIX}", "topic_name": f"projects/sdm-prod/topics/enterprise-{PROJECT_ID}", "token": { "refresh_token": "mock-refresh-token", @@ -388,7 +443,7 @@ async def test_app_credentials( ("sdm_managed_topic", "device_access_project_id", "cloud_project_id"), [(True, "new-project-id", "new-cloud-project-id")], ) -async def test_config_flow_restart(hass: HomeAssistant, oauth, subscriber) -> None: +async def test_config_flow_restart(hass: HomeAssistant, oauth: OAuthFixture) -> None: """Check with auth implementation is re-initialized when aborting the flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -443,8 +498,7 @@ async def test_config_flow_restart(hass: HomeAssistant, oauth, subscriber) -> No @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_config_flow_wrong_project_id( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Check the case where the wrong project ids are entered.""" result = await hass.config_entries.flow.async_init( @@ -500,8 +554,7 @@ async def test_config_flow_wrong_project_id( ) async def test_config_flow_pubsub_configuration_error( hass: HomeAssistant, - oauth, - mock_subscriber, + oauth: OAuthFixture, ) -> None: """Check full flow fails with configuration error.""" result = await hass.config_entries.flow.async_init( @@ -525,6 +578,11 @@ async def test_config_flow_pubsub_configuration_error( }, ) assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic_confirm" + assert not result.get("errors") + + result = await oauth.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pubsub_subscription" assert result.get("data_schema")({}) == { "subscription_name": "create_new_subscription", @@ -546,7 +604,8 @@ async def test_config_flow_pubsub_configuration_error( [(True, HTTPStatus.INTERNAL_SERVER_ERROR)], ) async def test_config_flow_pubsub_subscriber_error( - hass: HomeAssistant, oauth, mock_subscriber + hass: HomeAssistant, + oauth: OAuthFixture, ) -> None: """Check full flow with a subscriber error.""" result = await hass.config_entries.flow.async_init( @@ -569,6 +628,11 @@ async def test_config_flow_pubsub_subscriber_error( }, ) assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic_confirm" + assert not result.get("errors") + + result = await oauth.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pubsub_subscription" assert result.get("data_schema")({}) == { "subscription_name": "create_new_subscription", @@ -695,36 +759,6 @@ async def test_reauth_multiple_config_entries( assert entry.data.get("extra_data") -@pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) -async def test_pubsub_subscription_strip_whitespace( - hass: HomeAssistant, oauth, subscriber -) -> None: - """Check that project id has whitespace stripped on entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - await oauth.async_app_creds_flow( - result, cloud_project_id=" " + CLOUD_PROJECT_ID + " " - ) - oauth.async_mock_refresh() - result = await oauth.async_configure(result, {"code": "1234"}) - entry = await oauth.async_complete_pubsub_flow( - result, selected_topic="projects/sdm-prod/topics/enterprise-some-project-id" - ) - assert entry.title == "Import from configuration.yaml" - assert "token" in entry.data - entry.data["token"].pop("expires_at") - assert entry.unique_id == PROJECT_ID - assert entry.data["token"] == { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - } - assert "subscription_name" in entry.data - assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID - - @pytest.mark.parametrize( ("sdm_managed_topic", "create_subscription_status"), [(True, HTTPStatus.UNAUTHORIZED)], @@ -754,6 +788,11 @@ async def test_pubsub_subscription_auth_failure( }, ) assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic_confirm" + assert not result.get("errors") + + result = await oauth.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pubsub_subscription" assert result.get("data_schema")({}) == { "subscription_name": "create_new_subscription", @@ -776,10 +815,9 @@ async def test_pubsub_subscription_auth_failure( ) async def test_pubsub_subscriber_config_entry_reauth( hass: HomeAssistant, - oauth, - setup_platform, - subscriber, - config_entry, + oauth: OAuthFixture, + setup_platform: PlatformSetup, + config_entry: MockConfigEntry, ) -> None: """Test the pubsub subscriber id is preserved during reauth.""" await setup_platform() @@ -805,22 +843,21 @@ async def test_pubsub_subscriber_config_entry_reauth( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_config_entry_title_from_home( - hass: HomeAssistant, oauth, subscriber + hass: HomeAssistant, + oauth: OAuthFixture, + auth: FakeAuth, ) -> None: """Test that the Google Home name is used for the config entry title.""" - device_manager = await subscriber.async_get_device_manager() - device_manager.add_structure( - Structure.MakeStructure( - { - "name": f"enterprise/{PROJECT_ID}/structures/some-structure-id", - "traits": { - "sdm.structures.traits.Info": { - "customName": "Example Home", - }, + auth.structures.append( + { + "name": f"enterprise/{PROJECT_ID}/structures/some-structure-id", + "traits": { + "sdm.structures.traits.Info": { + "customName": "Example Home", }, - } - ) + }, + } ) result = await hass.config_entries.flow.async_init( @@ -838,7 +875,7 @@ async def test_config_entry_title_from_home( assert entry.data.get("cloud_project_id") == CLOUD_PROJECT_ID assert ( entry.data.get("subscription_name") - == f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUBSCRIBER_SUFFIX}" + == f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUFFIX}" ) assert ( entry.data.get("topic_name") @@ -848,13 +885,13 @@ async def test_config_entry_title_from_home( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_config_entry_title_multiple_homes( - hass: HomeAssistant, oauth, subscriber + hass: HomeAssistant, + oauth: OAuthFixture, + auth: FakeAuth, ) -> None: """Test handling of multiple Google Homes authorized.""" - - device_manager = await subscriber.async_get_device_manager() - device_manager.add_structure( - Structure.MakeStructure( + auth.structures.extend( + [ { "name": f"enterprise/{PROJECT_ID}/structures/id-1", "traits": { @@ -862,11 +899,7 @@ async def test_config_entry_title_multiple_homes( "customName": "Example Home #1", }, }, - } - ) - ) - device_manager.add_structure( - Structure.MakeStructure( + }, { "name": f"enterprise/{PROJECT_ID}/structures/id-2", "traits": { @@ -874,8 +907,8 @@ async def test_config_entry_title_multiple_homes( "customName": "Example Home #2", }, }, - } - ) + }, + ] ) result = await hass.config_entries.flow.async_init( @@ -914,7 +947,7 @@ async def test_title_failure_fallback( assert entry.data.get("cloud_project_id") == CLOUD_PROJECT_ID assert ( entry.data.get("subscription_name") - == f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUBSCRIBER_SUFFIX}" + == f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUFFIX}" ) assert ( entry.data.get("topic_name") @@ -923,18 +956,17 @@ async def test_title_failure_fallback( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) -async def test_structure_missing_trait(hass: HomeAssistant, oauth, subscriber) -> None: +async def test_structure_missing_trait( + hass: HomeAssistant, oauth: OAuthFixture, auth: FakeAuth +) -> None: """Test handling the case where a structure has no name set.""" - device_manager = await subscriber.async_get_device_manager() - device_manager.add_structure( - Structure.MakeStructure( - { - "name": f"enterprise/{PROJECT_ID}/structures/id-1", - # Missing Info trait - "traits": {}, - } - ) + auth.structures.append( + { + "name": f"enterprise/{PROJECT_ID}/structures/id-1", + # Missing Info trait + "traits": {}, + } ) result = await hass.config_entries.flow.async_init( @@ -973,8 +1005,7 @@ async def test_dhcp_discovery( @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_dhcp_discovery_with_creds( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Exercise discovery dhcp with no config present (can't run).""" result = await hass.config_entries.flow.async_init( @@ -1008,7 +1039,7 @@ async def test_dhcp_discovery_with_creds( "auth_implementation": "imported-cred", "cloud_project_id": CLOUD_PROJECT_ID, "project_id": PROJECT_ID, - "subscription_name": f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUBSCRIBER_SUFFIX}", + "subscription_name": f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUFFIX}", "topic_name": f"projects/sdm-prod/topics/enterprise-{PROJECT_ID}", "token": { "refresh_token": "mock-refresh-token", @@ -1029,7 +1060,6 @@ async def test_dhcp_discovery_with_creds( async def test_token_error( hass: HomeAssistant, oauth: OAuthFixture, - subscriber: FakeSubscriber, status_code: HTTPStatus, error_reason: str, ) -> None: @@ -1064,8 +1094,7 @@ async def test_token_error( ) async def test_existing_topic_and_subscription( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test selecting existing user managed topic and subscription.""" result = await hass.config_entries.flow.async_init( @@ -1103,10 +1132,9 @@ async def test_existing_topic_and_subscription( async def test_no_eligible_topics( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: - """Test the case where there are no eligible pub/sub topics.""" + """Test the case where there are no eligible pub/sub topics and the topic is created.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -1115,8 +1143,36 @@ async def test_no_eligible_topics( result = await oauth.async_configure(result, None) assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "pubsub" - assert result.get("errors") == {"base": "no_pubsub_topics"} + assert result.get("step_id") == "pubsub_topic" + assert not result.get("errors") + # Option shown to create a new topic + assert result.get("data_schema")({}) == { + "topic_name": "create_new_topic", + } + + entry = await oauth.async_complete_pubsub_flow( + result, + selected_topic="create_new_topic", + selected_subscription="create_new_subscription", + ) + + data = dict(entry.data) + assert "token" in data + data["token"].pop("expires_in") + data["token"].pop("expires_at") + assert data == { + "sdm": {}, + "auth_implementation": "imported-cred", + "cloud_project_id": CLOUD_PROJECT_ID, + "project_id": PROJECT_ID, + "subscription_name": f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUFFIX}", + "topic_name": f"projects/{CLOUD_PROJECT_ID}/topics/home-assistant-{RAND_SUFFIX}", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + }, + } @pytest.mark.parametrize( @@ -1127,8 +1183,7 @@ async def test_no_eligible_topics( ) async def test_list_topics_failure( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test selecting existing user managed topic and subscription.""" result = await hass.config_entries.flow.async_init( @@ -1137,11 +1192,90 @@ async def test_list_topics_failure( await oauth.async_app_creds_flow(result) oauth.async_mock_refresh() + result = await oauth.async_configure(result, None) + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "pubsub_api_error" + + +@pytest.mark.parametrize( + ("create_topic_status"), + [(HTTPStatus.INTERNAL_SERVER_ERROR)], +) +async def test_create_topic_failed( + hass: HomeAssistant, + oauth: OAuthFixture, + aioclient_mock: AiohttpClientMocker, + cloud_project_id: str, + subscriptions: list[tuple[str, str]], + auth: FakeAuth, +) -> None: + """Test the case where there are no eligible pub/sub topics and the topic is created.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await oauth.async_app_creds_flow(result) + oauth.async_mock_refresh() + result = await oauth.async_configure(result, None) assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "pubsub" + assert result.get("step_id") == "pubsub_topic" + assert not result.get("errors") + # Option shown to create a new topic + assert result.get("data_schema")({}) == { + "topic_name": "create_new_topic", + } + + result = await oauth.async_configure(result, {"topic_name": "create_new_topic"}) + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic" assert result.get("errors") == {"base": "pubsub_api_error"} + # Re-register mock requests needed for the rest of the test. The topic + # request will now succeed. + aioclient_mock.clear_requests() + setup_mock_create_topic_responses(aioclient_mock, cloud_project_id) + # Fix up other mock responses cleared above + auth.register_mock_requests() + setup_mock_list_subscriptions_responses( + aioclient_mock, + cloud_project_id, + subscriptions, + ) + setup_mock_create_subscription_responses(aioclient_mock, cloud_project_id) + + result = await oauth.async_configure(result, {"topic_name": "create_new_topic"}) + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic_confirm" + assert not result.get("errors") + + result = await oauth.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_subscription" + assert not result.get("errors") + + # Create a subscription for the topic and end the flow + entry = await oauth.async_finish_setup( + result, + {"subscription_name": "create_new_subscription"}, + ) + data = dict(entry.data) + assert "token" in data + data["token"].pop("expires_in") + data["token"].pop("expires_at") + assert data == { + "sdm": {}, + "auth_implementation": "imported-cred", + "cloud_project_id": CLOUD_PROJECT_ID, + "project_id": PROJECT_ID, + "subscription_name": f"projects/{CLOUD_PROJECT_ID}/subscriptions/home-assistant-{RAND_SUFFIX}", + "topic_name": f"projects/{CLOUD_PROJECT_ID}/topics/home-assistant-{RAND_SUFFIX}", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + }, + } + @pytest.mark.parametrize( ("sdm_managed_topic", "list_subscriptions_status"), @@ -1151,8 +1285,7 @@ async def test_list_topics_failure( ) async def test_list_subscriptions_failure( hass: HomeAssistant, - oauth, - subscriber, + oauth: OAuthFixture, ) -> None: """Test selecting existing user managed topic and subscription.""" result = await hass.config_entries.flow.async_init( @@ -1174,5 +1307,10 @@ async def test_list_subscriptions_failure( }, ) assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "pubsub_topic_confirm" + assert not result.get("errors") + + result = await oauth.async_configure(result, {}) + assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "pubsub_subscription" assert result.get("errors") == {"base": "pubsub_api_error"} diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index cf0e1c5ecce..33f5c1637fa 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -1,8 +1,8 @@ """The tests for Nest device triggers.""" from typing import Any +from unittest.mock import AsyncMock -from google_nest_sdm.event import EventMessage import pytest from pytest_unordered import unordered @@ -18,7 +18,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .common import DEVICE_ID, CreateDevice, PlatformSetup, create_nest_event from tests.common import async_get_device_automations @@ -447,7 +447,7 @@ async def test_subscriber_automation( service_calls: list[ServiceCall], create_device: CreateDevice, setup_platform: PlatformSetup, - subscriber: FakeSubscriber, + subscriber: AsyncMock, ) -> None: """Test end to end subscriber triggers automation.""" create_device.create( @@ -465,7 +465,7 @@ async def test_subscriber_automation( assert await setup_automation(hass, device_entry.id, "camera_motion") # Simulate a pubsub message received by the subscriber with a motion event - event = EventMessage.create_event( + event = create_nest_event( { "eventId": "some-event-id", "timestamp": "2019-01-01T00:00:01Z", @@ -479,7 +479,6 @@ async def test_subscriber_automation( }, }, }, - auth=None, ) await subscriber.async_receive_event(event) await hass.async_block_till_done() diff --git a/tests/components/nest/test_event.py b/tests/components/nest/test_event.py index f45e6c1c6e6..942f19d6626 100644 --- a/tests/components/nest/test_event.py +++ b/tests/components/nest/test_event.py @@ -2,7 +2,7 @@ import datetime from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from google_nest_sdm.event import EventMessage, EventType @@ -13,7 +13,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow -from .common import DEVICE_ID, CreateDevice, FakeSubscriber +from .common import DEVICE_ID, TEST_CLIP_URL, CreateDevice, create_nest_event from .conftest import PlatformSetup EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." @@ -31,14 +31,6 @@ def platforms() -> list[Platform]: return [Platform.EVENT] -@pytest.fixture(autouse=True) -def enable_prefetch(subscriber: FakeSubscriber) -> None: - """Fixture to enable media fetching for tests to exercise.""" - subscriber.cache_policy.fetch = True - with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=5): - yield - - @pytest.fixture def device_type() -> str: """Fixture for the type of device under test.""" @@ -80,7 +72,7 @@ def create_event_messages( events: dict[str, Any], parameters: dict[str, Any] | None = None ) -> EventMessage: """Create an EventMessage for events.""" - return EventMessage.create_event( + return create_nest_event( { "eventId": "some-event-id", "timestamp": utcnow().isoformat(timespec="seconds"), @@ -90,7 +82,6 @@ def create_event_messages( }, **(parameters if parameters else {}), }, - auth=None, ) @@ -152,7 +143,7 @@ def create_event_messages( ) async def test_receive_events( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, setup_platform: PlatformSetup, create_device: CreateDevice, trait_types: list[TraitType], @@ -192,7 +183,7 @@ async def test_receive_events( @pytest.mark.parametrize(("trait_type"), [(TraitType.DOORBELL_CHIME)]) async def test_ignore_unrelated_event( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, setup_platform: PlatformSetup, create_device: CreateDevice, trait_type: TraitType, @@ -222,7 +213,7 @@ async def test_ignore_unrelated_event( @pytest.mark.freeze_time("2024-08-24T12:00:00Z") async def test_event_threads( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, setup_platform: PlatformSetup, create_device: CreateDevice, freezer: FrozenDateTimeFactory, @@ -275,7 +266,7 @@ async def test_event_threads( }, EventType.CAMERA_CLIP_PREVIEW: { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "http://example", + "previewUrl": TEST_CLIP_URL, }, }, parameters={"eventThreadState": "ENDED"}, @@ -306,7 +297,7 @@ async def test_event_threads( }, EventType.CAMERA_CLIP_PREVIEW: { "eventSessionId": EVENT_SESSION_ID2, - "previewUrl": "http://example", + "previewUrl": TEST_CLIP_URL, }, }, parameters={"eventThreadState": "ENDED"}, diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index e746e5f263f..d4ad81bd4e8 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -9,26 +9,38 @@ from __future__ import annotations from collections.abc import Mapping import datetime from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock -from google_nest_sdm.device import Device -from google_nest_sdm.event import EventMessage +import aiohttp import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from .common import CreateDevice +from .common import ( + DEVICE_ID, + TEST_CLIP_URL, + TEST_IMAGE_URL, + CreateDevice, + PlatformSetup, + create_nest_event, +) from tests.common import async_capture_events DOMAIN = "nest" -DEVICE_ID = "some-device-id" PLATFORM = "camera" NEST_EVENT = "nest_event" EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." +GENERATE_IMAGE_URL_RESPONSE = { + "results": { + "url": TEST_IMAGE_URL, + "token": "g.0.eventToken", + }, +} +IMAGE_BYTES_FROM_EVENT = b"test url image bytes" EVENT_KEYS = {"device_id", "type", "timestamp", "zones"} @@ -104,7 +116,7 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None): """Create an EventMessage for events.""" if not timestamp: timestamp = utcnow() - return EventMessage.create_event( + return create_nest_event( { "eventId": "some-event-id", "timestamp": timestamp.isoformat(timespec="seconds"), @@ -113,7 +125,6 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None): "events": events, }, }, - auth=None, ) @@ -167,7 +178,7 @@ async def test_event( entry = entity_registry.async_get("camera.front") assert entry is not None - assert entry.unique_id == "some-device-id-camera" + assert entry.unique_id == f"{DEVICE_ID}-camera" assert entry.domain == "camera" device = device_registry.async_get(entry.device_id) @@ -175,6 +186,11 @@ async def test_event( assert device.model == expected_model assert device.identifiers == {("nest", DEVICE_ID)} + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + timestamp = utcnow() await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp)) await hass.async_block_till_done() @@ -300,12 +316,11 @@ async def test_event_message_without_device_event( events = async_capture_events(hass, NEST_EVENT) await setup_platform() timestamp = utcnow() - event = EventMessage.create_event( + event = create_nest_event( { "eventId": "some-event-id", "timestamp": timestamp.isoformat(timespec="seconds"), }, - auth=None, ) await subscriber.async_receive_event(event) await hass.async_block_till_done() @@ -339,7 +354,7 @@ async def test_doorbell_event_thread( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "image-url-1", + "previewUrl": TEST_CLIP_URL, }, }, }, @@ -356,9 +371,7 @@ async def test_doorbell_event_thread( "eventThreadState": "STARTED", } ) - await subscriber.async_receive_event( - EventMessage.create_event(message_data_1, auth=None) - ) + await subscriber.async_receive_event(create_nest_event(message_data_1)) # Publish message #2 that sends a no-op update to end the event thread timestamp2 = timestamp1 + datetime.timedelta(seconds=1) @@ -369,9 +382,7 @@ async def test_doorbell_event_thread( "eventThreadState": "ENDED", } ) - await subscriber.async_receive_event( - EventMessage.create_event(message_data_2, auth=None) - ) + await subscriber.async_receive_event(create_nest_event(message_data_2)) await hass.async_block_till_done() # The event is only published once @@ -415,7 +426,7 @@ async def test_doorbell_event_session_update( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "image-url-1", + "previewUrl": TEST_CLIP_URL, }, }, timestamp=timestamp1, @@ -437,7 +448,7 @@ async def test_doorbell_event_session_update( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, - "previewUrl": "image-url-1", + "previewUrl": TEST_CLIP_URL, }, }, timestamp=timestamp2, @@ -459,7 +470,11 @@ async def test_doorbell_event_session_update( async def test_structure_update_event( - hass: HomeAssistant, entity_registry: er.EntityRegistry, subscriber, setup_platform + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subscriber: AsyncMock, + setup_platform: PlatformSetup, + create_device: CreateDevice, ) -> None: """Test a pubsub message for a new device being added.""" events = async_capture_events(hass, NEST_EVENT) @@ -468,8 +483,8 @@ async def test_structure_update_event( # Entity for first device is registered assert entity_registry.async_get("camera.front") - new_device = Device.MakeDevice( - { + create_device.create( + raw_data={ "name": "device-id-2", "type": "sdm.devices.types.CAMERA", "traits": { @@ -479,16 +494,13 @@ async def test_structure_update_event( "sdm.devices.traits.CameraLiveStream": {}, }, }, - auth=None, ) - device_manager = await subscriber.async_get_device_manager() - device_manager.add_device(new_device) # Entity for new devie has not yet been loaded assert not entity_registry.async_get("camera.back") # Send a message that triggers the device to be loaded - message = EventMessage.create_event( + message = create_nest_event( { "eventId": "some-event-id", "timestamp": utcnow().isoformat(timespec="seconds"), @@ -498,17 +510,10 @@ async def test_structure_update_event( "object": "enterprise/example/devices/some-device-id2", }, }, - auth=None, ) - with ( - patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), - patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ), - ): - await subscriber.async_receive_event(message) - await hass.async_block_till_done() + + await subscriber.async_receive_event(message) + await hass.async_block_till_done() # No home assistant events published assert not events diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index 17ddc485e85..c7ac5875403 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -9,10 +9,12 @@ relevant modes. """ from collections.abc import Generator +import datetime +from http import HTTPStatus import logging -from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +import aiohttp from google_nest_sdm.exceptions import ( ApiException, AuthException, @@ -22,6 +24,7 @@ from google_nest_sdm.exceptions import ( import pytest from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -29,13 +32,15 @@ from .common import ( PROJECT_ID, SUBSCRIBER_ID, TEST_CONFIG_NEW_SUBSCRIPTION, - FakeSubscriber, PlatformSetup, - YieldFixture, ) +from tests.test_util.aiohttp import AiohttpClientMocker + PLATFORM = "sensor" +EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp() + @pytest.fixture def platforms() -> list[str]: @@ -61,25 +66,6 @@ def warning_caplog( yield caplog -@pytest.fixture -def subscriber_side_effect() -> Any | None: - """Fixture to inject failures into FakeSubscriber start.""" - return None - - -@pytest.fixture -def failing_subscriber( - subscriber_side_effect: Any | None, -) -> YieldFixture[FakeSubscriber]: - """Fixture overriding default subscriber behavior to allow failure injection.""" - subscriber = FakeSubscriber() - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", - side_effect=subscriber_side_effect, - ): - yield subscriber - - async def test_setup_success( hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_platform ) -> None: @@ -125,10 +111,9 @@ async def test_setup_configuration_failure( @pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()]) -async def test_setup_susbcriber_failure( +async def test_setup_subscriber_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - failing_subscriber, setup_base_platform, ) -> None: """Test configuration error.""" @@ -145,7 +130,6 @@ async def test_setup_device_manager_failure( ) -> None: """Test device manager api failure.""" with ( - patch("homeassistant.components.nest.api.GoogleNestSubscriber.start_async"), patch( "homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager", side_effect=ApiException(), @@ -160,12 +144,60 @@ async def test_setup_device_manager_failure( assert entries[0].state is ConfigEntryState.SETUP_RETRY +@pytest.mark.parametrize("token_expiration_time", [EXPIRED_TOKEN_TIMESTAMP]) +@pytest.mark.parametrize( + ("token_response_args", "expected_state", "expected_steps"), + [ + # Cases that retry integration setup + ( + {"status": HTTPStatus.INTERNAL_SERVER_ERROR}, + ConfigEntryState.SETUP_RETRY, + [], + ), + ({"exc": aiohttp.ClientError("No internet")}, ConfigEntryState.SETUP_RETRY, []), + # Cases that require the user to reauthenticate in a config flow + ( + {"status": HTTPStatus.BAD_REQUEST}, + ConfigEntryState.SETUP_ERROR, + ["reauth_confirm"], + ), + ( + {"status": HTTPStatus.FORBIDDEN}, + ConfigEntryState.SETUP_ERROR, + ["reauth_confirm"], + ), + ], +) +async def test_expired_token_refresh_error( + hass: HomeAssistant, + setup_base_platform: PlatformSetup, + aioclient_mock: AiohttpClientMocker, + token_response_args: dict, + expected_state: ConfigEntryState, + expected_steps: list[str], +) -> None: + """Test errors when attempting to refresh the auth token.""" + + aioclient_mock.post( + OAUTH2_TOKEN, + **token_response_args, + ) + + await setup_base_platform() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is expected_state + + flows = hass.config_entries.flow.async_progress() + assert expected_steps == [flow["step_id"] for flow in flows] + + @pytest.mark.parametrize("subscriber_side_effect", [AuthException()]) async def test_subscriber_auth_failure( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, setup_base_platform, - failing_subscriber, ) -> None: """Test subscriber throws an authentication error.""" await setup_base_platform() @@ -184,7 +216,6 @@ async def test_subscriber_configuration_failure( hass: HomeAssistant, error_caplog: pytest.LogCaptureFixture, setup_base_platform, - failing_subscriber, ) -> None: """Test configuration error.""" await setup_base_platform() @@ -210,14 +241,12 @@ async def test_unload_entry(hass: HomeAssistant, setup_platform) -> None: async def test_remove_entry( hass: HomeAssistant, - setup_base_platform, + setup_base_platform: PlatformSetup, + aioclient_mock: AiohttpClientMocker, + subscriber: AsyncMock, ) -> None: """Test successful unload of a ConfigEntry.""" - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=FakeSubscriber(), - ): - await setup_base_platform() + await setup_base_platform() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -227,14 +256,18 @@ async def test_remove_entry( assert entry.data.get("subscriber_id") == SUBSCRIBER_ID assert entry.data.get("project_id") == PROJECT_ID - with ( - patch("homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id"), - patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", - ) as delete, - ): - assert await hass.config_entries.async_remove(entry.entry_id) - assert delete.called + aioclient_mock.clear_requests() + aioclient_mock.delete( + f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}", + json={}, + ) + + assert not subscriber.stop.called + + assert await hass.config_entries.async_remove(entry.entry_id) + + assert aioclient_mock.call_count == 1 + assert subscriber.stop.called entries = hass.config_entries.async_entries(DOMAIN) assert not entries @@ -243,7 +276,7 @@ async def test_remove_entry( async def test_home_assistant_stop( hass: HomeAssistant, setup_platform: PlatformSetup, - subscriber: FakeSubscriber, + subscriber: AsyncMock, ) -> None: """Test successful subscriber shutdown when HomeAssistant stops.""" await setup_platform() @@ -253,31 +286,37 @@ async def test_home_assistant_stop( entry = entries[0] assert entry.state is ConfigEntryState.LOADED + assert not subscriber.stop.called await hass.async_stop() - assert subscriber.stop_calls == 1 + assert subscriber.stop.called async def test_remove_entry_delete_subscriber_failure( - hass: HomeAssistant, setup_base_platform + hass: HomeAssistant, + setup_base_platform: PlatformSetup, + aioclient_mock: AiohttpClientMocker, + subscriber: AsyncMock, ) -> None: """Test a failure when deleting the subscription.""" - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=FakeSubscriber(), - ): - await setup_base_platform() + await setup_base_platform() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", - side_effect=SubscriberException(), - ) as delete: - assert await hass.config_entries.async_remove(entry.entry_id) - assert delete.called + aioclient_mock.clear_requests() + aioclient_mock.delete( + f"https://pubsub.googleapis.com/v1/{SUBSCRIBER_ID}", + status=HTTPStatus.NOT_FOUND, + ) + + assert not subscriber.stop.called + + assert await hass.config_entries.async_remove(entry.entry_id) + + assert aioclient_mock.call_count == 1 + assert subscriber.stop.called entries = hass.config_entries.async_entries(DOMAIN) assert not entries diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 2526bfdf975..d009e1185da 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -13,7 +13,6 @@ from unittest.mock import patch import aiohttp import av -from google_nest_sdm.event import EventMessage import numpy as np import pytest @@ -29,9 +28,16 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util -from .common import DEVICE_ID, CreateDevice, FakeSubscriber +from .common import ( + DEVICE_ID, + TEST_CLIP_URL, + TEST_IMAGE_URL, + CreateDevice, + create_nest_event, +) +from .conftest import FakeAuth from tests.common import MockUser, async_capture_events from tests.typing import ClientSessionGenerator @@ -70,7 +76,6 @@ BATTERY_CAMERA_TRAITS = { PERSON_EVENT = "sdm.devices.events.CameraPerson.Person" MOTION_EVENT = "sdm.devices.events.CameraMotion.Motion" -TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." GENERATE_IMAGE_URL_RESPONSE = { "results": { "url": TEST_IMAGE_URL, @@ -162,12 +167,6 @@ def mp4() -> io.BytesIO: return output -@pytest.fixture(autouse=True) -def enable_prefetch(subscriber: FakeSubscriber) -> None: - """Fixture to enable media fetching for tests to exercise.""" - subscriber.cache_policy.fetch = True - - @pytest.fixture def cache_size() -> int: """Fixture for overrideing cache size.""" @@ -200,7 +199,7 @@ def create_event_message(event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" if device_id is None: device_id = DEVICE_ID - return EventMessage.create_event( + return create_nest_event( { "eventId": f"{EVENT_ID}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), @@ -209,7 +208,6 @@ def create_event_message(event_data, timestamp, device_id=None): "events": event_data, }, }, - auth=None, ) @@ -224,7 +222,7 @@ def create_battery_event_data( }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": event_session_id, - "previewUrl": "https://127.0.0.1/example", + "previewUrl": TEST_CLIP_URL, }, } @@ -284,7 +282,9 @@ async def test_supported_device( assert len(browse.children) == 0 -async def test_integration_unloaded(hass: HomeAssistant, auth, setup_platform) -> None: +async def test_integration_unloaded( + hass: HomeAssistant, auth: FakeAuth, setup_platform +) -> None: """Test the media player loads, but has no devices, when config unloaded.""" await setup_platform() @@ -1012,9 +1012,9 @@ async def test_media_permission_unauthorized( client = await hass_client() response = await client.get(media_url) - assert ( - response.status == HTTPStatus.UNAUTHORIZED - ), f"Response not matched: {response}" + assert response.status == HTTPStatus.UNAUTHORIZED, ( + f"Response not matched: {response}" + ) async def test_multiple_devices( @@ -1257,6 +1257,7 @@ async def test_media_store_save_filesystem_error( assert response.status == HTTPStatus.NOT_FOUND, f"Response not matched: {response}" +@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS]) async def test_media_store_load_filesystem_error( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -1305,9 +1306,9 @@ async def test_media_store_load_filesystem_error( response = await client.get( f"/api/nest/event_media/{device.id}/{event_identifier}" ) - assert ( - response.status == HTTPStatus.NOT_FOUND - ), f"Response not matched: {response}" + assert response.status == HTTPStatus.NOT_FOUND, ( + f"Response not matched: {response}" + ) @pytest.mark.parametrize(("device_traits", "cache_size"), [(BATTERY_CAMERA_TRAITS, 5)]) diff --git a/tests/components/nest/test_sensor.py b/tests/components/nest/test_sensor.py index 2339d72ebc7..8bda1d387f8 100644 --- a/tests/components/nest/test_sensor.py +++ b/tests/components/nest/test_sensor.py @@ -5,8 +5,8 @@ pubsub subscriber. """ from typing import Any +from unittest.mock import AsyncMock -from google_nest_sdm.event import EventMessage import pytest from homeassistant.components.sensor import ( @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup +from .common import DEVICE_ID, CreateDevice, PlatformSetup, create_nest_event @pytest.fixture @@ -198,7 +198,7 @@ async def test_device_name_from_structure( async def test_event_updates_sensor( hass: HomeAssistant, - subscriber: FakeSubscriber, + subscriber: AsyncMock, create_device: CreateDevice, setup_platform: PlatformSetup, ) -> None: @@ -217,7 +217,7 @@ async def test_event_updates_sensor( assert temperature.state == "25.1" # Simulate a pubsub message received by the subscriber with a trait update - event = EventMessage.create_event( + event = create_nest_event( { "eventId": "some-event-id", "timestamp": "2019-01-01T00:00:01Z", @@ -230,7 +230,6 @@ async def test_event_updates_sensor( }, }, }, - auth=None, ) await subscriber.async_receive_event(event) await hass.async_block_till_done() # Process dispatch/update signal diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 730cb0cb117..9110f8c724f 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -11,7 +11,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.webhook import async_handle_webhook from homeassistant.const import Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.util.aiohttp import MockRequest from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/netatmo/snapshots/test_binary_sensor.ambr b/tests/components/netatmo/snapshots/test_binary_sensor.ambr index 6a90b4dd77a..3066c999655 100644 --- a/tests/components/netatmo/snapshots/test_binary_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -106,6 +108,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -156,6 +159,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -206,6 +210,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -256,6 +261,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -304,6 +310,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -352,6 +359,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -402,6 +410,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -450,6 +459,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -498,6 +508,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_button.ambr b/tests/components/netatmo/snapshots/test_button.ambr new file mode 100644 index 00000000000..086403c3b69 --- /dev/null +++ b/tests/components/netatmo/snapshots/test_button.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_entity[button.bubendorff_blind_preferred_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.bubendorff_blind_preferred_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Preferred position', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'preferred_position', + 'unique_id': '0009999993-DeviceType.NBO-preferred_position', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[button.bubendorff_blind_preferred_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Bubendorff blind Preferred position', + }), + 'context': , + 'entity_id': 'button.bubendorff_blind_preferred_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entity[button.entrance_blinds_preferred_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.entrance_blinds_preferred_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Preferred position', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'preferred_position', + 'unique_id': '0009999992-DeviceType.NBR-preferred_position', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[button.entrance_blinds_preferred_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'friendly_name': 'Entrance Blinds Preferred position', + }), + 'context': , + 'entity_id': 'button.entrance_blinds_preferred_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/netatmo/snapshots/test_camera.ambr b/tests/components/netatmo/snapshots/test_camera.ambr index 94a5ded5031..9bd10ed9b5f 100644 --- a/tests/components/netatmo/snapshots/test_camera.ambr +++ b/tests/components/netatmo/snapshots/test_camera.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -128,6 +130,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_climate.ambr b/tests/components/netatmo/snapshots/test_climate.ambr index aeae1fd71c7..506e0fb5590 100644 --- a/tests/components/netatmo/snapshots/test_climate.ambr +++ b/tests/components/netatmo/snapshots/test_climate.ambr @@ -20,6 +20,7 @@ 'target_temp_step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -95,6 +96,7 @@ 'target_temp_step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -176,6 +178,7 @@ 'target_temp_step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -256,6 +259,7 @@ 'target_temp_step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -338,6 +342,7 @@ 'target_temp_step': 0.5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_cover.ambr b/tests/components/netatmo/snapshots/test_cover.ambr index 7ea016f5ae8..46aafb32e8e 100644 --- a/tests/components/netatmo/snapshots/test_cover.ambr +++ b/tests/components/netatmo/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_fan.ambr b/tests/components/netatmo/snapshots/test_fan.ambr index ba882d68e50..f850f7ada3b 100644 --- a/tests/components/netatmo/snapshots/test_fan.ambr +++ b/tests/components/netatmo/snapshots/test_fan.ambr @@ -11,6 +11,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_init.ambr b/tests/components/netatmo/snapshots/test_init.ambr index 60cb22d74f2..35e7f7efc29 100644 --- a/tests/components/netatmo/snapshots/test_init.ambr +++ b/tests/components/netatmo/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/control', 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/control', 'connections': set({ }), @@ -67,6 +69,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/control', 'connections': set({ }), @@ -99,6 +102,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'corridor', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -131,6 +135,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -163,6 +168,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/control', 'connections': set({ }), @@ -195,6 +201,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -227,6 +234,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -259,6 +267,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -291,6 +300,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -323,6 +333,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -355,6 +366,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -387,6 +399,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -419,6 +432,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -451,6 +465,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -483,6 +498,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -515,6 +531,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/security', 'connections': set({ }), @@ -547,6 +564,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -579,6 +597,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/security', 'connections': set({ }), @@ -611,6 +630,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://home.netatmo.com/security', 'connections': set({ }), @@ -643,6 +663,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -675,6 +696,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -707,6 +729,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -739,6 +762,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -771,6 +795,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -803,6 +828,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -835,6 +861,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -867,6 +894,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -899,6 +927,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -931,6 +960,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -963,6 +993,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/weather', 'connections': set({ }), @@ -995,6 +1026,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'bureau', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -1027,6 +1059,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'livingroom', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -1059,6 +1092,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'entrada', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -1091,6 +1125,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'cocina', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -1123,6 +1158,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://my.netatmo.com/app/energy', 'connections': set({ }), @@ -1155,6 +1191,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://weathermap.netatmo.com/', 'connections': set({ }), @@ -1187,6 +1224,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://weathermap.netatmo.com/', 'connections': set({ }), @@ -1219,6 +1257,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://weathermap.netatmo.com/', 'connections': set({ }), diff --git a/tests/components/netatmo/snapshots/test_light.ambr b/tests/components/netatmo/snapshots/test_light.ambr index fe5a8aac7d0..cc7da6e8712 100644 --- a/tests/components/netatmo/snapshots/test_light.ambr +++ b/tests/components/netatmo/snapshots/test_light.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -121,6 +123,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_select.ambr b/tests/components/netatmo/snapshots/test_select.ambr index ff68fc71c09..d98d9adb87f 100644 --- a/tests/components/netatmo/snapshots/test_select.ambr +++ b/tests/components/netatmo/snapshots/test_select.ambr @@ -11,6 +11,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index ba18c2ca21a..b149e80fa5b 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -68,6 +69,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -128,6 +130,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -187,6 +190,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +245,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -293,6 +298,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -342,6 +348,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -393,6 +400,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -448,6 +456,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -497,6 +506,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -548,6 +558,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -606,6 +617,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -664,6 +676,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -721,6 +734,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -773,6 +787,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -823,6 +838,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -870,6 +886,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -921,6 +938,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -974,6 +992,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1021,6 +1040,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1072,6 +1092,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1122,6 +1143,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1169,6 +1191,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1218,6 +1241,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1270,6 +1294,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1320,6 +1345,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1367,6 +1393,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1416,6 +1443,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1476,6 +1504,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1529,6 +1558,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1583,6 +1613,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1637,6 +1668,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1690,6 +1722,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1744,6 +1777,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1801,6 +1835,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1855,6 +1890,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1912,6 +1948,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1966,6 +2003,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2026,6 +2064,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2079,6 +2118,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2133,6 +2173,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2187,6 +2228,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2240,6 +2282,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2294,6 +2337,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2351,6 +2395,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2405,6 +2450,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2462,6 +2508,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2516,6 +2563,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2576,6 +2624,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2629,6 +2678,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2683,6 +2733,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2737,6 +2788,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2790,6 +2842,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2844,6 +2897,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2901,6 +2955,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2955,6 +3010,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3012,6 +3068,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3064,6 +3121,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3113,6 +3171,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3173,6 +3232,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3233,6 +3293,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3292,6 +3353,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3346,6 +3408,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3398,6 +3461,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3447,6 +3511,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3498,6 +3563,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3553,6 +3619,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3602,6 +3669,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3651,6 +3719,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3698,6 +3767,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3745,6 +3815,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3792,6 +3863,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3839,6 +3911,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3888,6 +3961,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3948,6 +4022,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4000,6 +4075,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4060,6 +4136,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4119,6 +4196,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4173,6 +4251,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4225,6 +4304,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4274,6 +4354,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4325,6 +4406,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4380,6 +4462,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4429,6 +4512,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4480,6 +4564,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4540,6 +4625,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4600,6 +4686,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4659,6 +4746,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4713,6 +4801,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4765,6 +4854,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4814,6 +4904,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4865,6 +4956,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4920,6 +5012,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4969,6 +5062,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5018,6 +5112,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5067,6 +5162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5117,6 +5213,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5166,6 +5263,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5218,6 +5316,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5270,6 +5369,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5330,6 +5430,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5382,6 +5483,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5434,6 +5536,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5484,6 +5587,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5531,6 +5635,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5580,6 +5685,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5633,6 +5739,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5682,6 +5789,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5734,6 +5842,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5786,6 +5895,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5836,6 +5946,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5883,6 +5994,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5932,6 +6044,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -5985,6 +6098,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6034,6 +6148,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6088,6 +6203,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6140,6 +6256,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6200,6 +6317,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6260,6 +6378,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6310,6 +6429,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6357,6 +6477,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6406,6 +6527,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6466,6 +6588,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6526,6 +6649,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6578,6 +6702,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6632,6 +6757,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6686,6 +6812,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6738,6 +6865,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6788,6 +6916,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6835,6 +6964,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6884,6 +7014,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6937,6 +7068,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -6984,6 +7116,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7035,6 +7168,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7087,6 +7221,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7139,6 +7274,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7194,6 +7330,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7244,6 +7381,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7291,6 +7429,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7338,6 +7477,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7389,6 +7529,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7444,6 +7585,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -7493,6 +7635,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/snapshots/test_switch.ambr b/tests/components/netatmo/snapshots/test_switch.ambr index 4244917d86f..f44cbcd22a5 100644 --- a/tests/components/netatmo/snapshots/test_switch.ambr +++ b/tests/components/netatmo/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/netatmo/test_button.py b/tests/components/netatmo/test_button.py new file mode 100644 index 00000000000..bffecf7d83a --- /dev/null +++ b/tests/components/netatmo/test_button.py @@ -0,0 +1,72 @@ +"""The tests for Netatmo button.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .common import selected_platforms, snapshot_platform_entities + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity( + hass: HomeAssistant, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test entities.""" + await snapshot_platform_entities( + hass, + config_entry, + Platform.BUTTON, + entity_registry, + snapshot, + ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_setup_and_services( + hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock +) -> None: + """Test setup and services.""" + with selected_platforms([Platform.BUTTON]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + button_entity = "button.entrance_blinds_preferred_position" + + assert hass.states.get(button_entity).state == STATE_UNKNOWN + + # Test button press + with patch("pyatmo.home.Home.async_set_state") as mock_set_state: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: button_entity}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + { + "modules": [ + { + "id": "0009999992", + "target_position": -2, + "bridge": "12:34:56:30:d5:d4", + } + ] + } + ) + + assert (state := hass.states.get(button_entity)) + assert state.state != STATE_UNKNOWN diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 43904ed8f71..32f20544043 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -19,7 +19,7 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from .common import ( diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index dc0312f7acd..18c811fd76b 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -41,7 +41,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from .common import selected_platforms, simulate_webhook, snapshot_platform_entities diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 436f75b12ec..f5714d69a98 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -7,7 +7,6 @@ from pyatmo.const import ALL_SCOPES import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( CONF_NEW_AREA, @@ -20,6 +19,10 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from .conftest import CLIENT_ID @@ -46,13 +49,13 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.5"), ip_addresses=[ip_address("192.168.1.5")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ) diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py index 509c1de736e..9368a564afb 100644 --- a/tests/components/netatmo/test_cover.py +++ b/tests/components/netatmo/test_cover.py @@ -14,7 +14,7 @@ from homeassistant.components.cover import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import selected_platforms, snapshot_platform_entities diff --git a/tests/components/netatmo/test_fan.py b/tests/components/netatmo/test_fan.py index 989ea1ac364..3dbc8b3a6f5 100644 --- a/tests/components/netatmo/test_fan.py +++ b/tests/components/netatmo/test_fan.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import selected_platforms, snapshot_platform_entities diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index c90d67e7630..0932395b8ec 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ( from homeassistant.components.netatmo import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import ( FAKE_WEBHOOK_ACTIVATION, diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index 274113405f6..458115f8f5c 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -17,7 +17,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import selected_platforms, simulate_webhook, snapshot_platform_entities diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py index dd82fad3d08..837f6201b1e 100644 --- a/tests/components/netatmo/test_switch.py +++ b/tests/components/netatmo/test_switch.py @@ -11,7 +11,7 @@ from homeassistant.components.switch import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import selected_platforms, snapshot_platform_entities diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 724a0568580..3c83bb57c43 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import Mock, patch from pynetgear import DEFAULT_USER import pytest -from homeassistant.components import ssdp from homeassistant.components.netgear.const import ( CONF_CONSIDER_HOME, DOMAIN, @@ -23,6 +22,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_MODEL_NUMBER, + ATTR_UPNP_PRESENTATION_URL, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -208,14 +213,14 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_URL_SLL, upnp={ - ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", - ssdp.ATTR_UPNP_PRESENTATION_URL: URL, - ssdp.ATTR_UPNP_SERIAL: SERIAL, + ATTR_UPNP_MODEL_NUMBER: "RBR20", + ATTR_UPNP_PRESENTATION_URL: URL, + ATTR_UPNP_SERIAL: SERIAL, }, ), ) @@ -228,13 +233,13 @@ async def test_ssdp_no_serial(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_URL, upnp={ - ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", - ssdp.ATTR_UPNP_PRESENTATION_URL: URL, + ATTR_UPNP_MODEL_NUMBER: "RBR20", + ATTR_UPNP_PRESENTATION_URL: URL, }, ), ) @@ -253,14 +258,14 @@ async def test_ssdp_ipv6(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_URLipv6, upnp={ - ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", - ssdp.ATTR_UPNP_PRESENTATION_URL: URL, - ssdp.ATTR_UPNP_SERIAL: SERIAL, + ATTR_UPNP_MODEL_NUMBER: "RBR20", + ATTR_UPNP_PRESENTATION_URL: URL, + ATTR_UPNP_SERIAL: SERIAL, }, ), ) @@ -273,14 +278,14 @@ async def test_ssdp(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_URL, upnp={ - ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", - ssdp.ATTR_UPNP_PRESENTATION_URL: URL, - ssdp.ATTR_UPNP_SERIAL: SERIAL, + ATTR_UPNP_MODEL_NUMBER: "RBR20", + ATTR_UPNP_PRESENTATION_URL: URL, + ATTR_UPNP_SERIAL: SERIAL, }, ), ) @@ -305,14 +310,14 @@ async def test_ssdp_port_5555(hass: HomeAssistant, service) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_URL_SLL, upnp={ - ssdp.ATTR_UPNP_MODEL_NUMBER: MODELS_PORT_5555[0], - ssdp.ATTR_UPNP_PRESENTATION_URL: URL_SSL, - ssdp.ATTR_UPNP_SERIAL: SERIAL, + ATTR_UPNP_MODEL_NUMBER: MODELS_PORT_5555[0], + ATTR_UPNP_PRESENTATION_URL: URL_SSL, + ATTR_UPNP_SERIAL: SERIAL, }, ), ) diff --git a/tests/components/netgear_lte/snapshots/test_init.ambr b/tests/components/netgear_lte/snapshots/test_init.ambr index ca65c17cc8e..2a806be8ae1 100644 --- a/tests/components/netgear_lte/snapshots/test_init.ambr +++ b/tests/components/netgear_lte/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://192.168.5.1', 'connections': set({ }), diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py index 1bd3dff1eff..e853109e33e 100644 --- a/tests/components/netgear_lte/test_init.py +++ b/tests/components/netgear_lte/test_init.py @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import CONF_DATA diff --git a/tests/components/network/__init__.py b/tests/components/network/__init__.py index f3ccacbd064..bbac7ca2f7c 100644 --- a/tests/components/network/__init__.py +++ b/tests/components/network/__init__.py @@ -1 +1,4 @@ """Tests for the Network Configuration integration.""" + +NO_LOOPBACK_IPADDR = "192.168.1.5" +LOOPBACK_IPADDR = "127.0.0.1" diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index d5fbb95a814..db2f268e968 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -1,14 +1,51 @@ """Tests for the Network Configuration integration.""" from collections.abc import Generator -from unittest.mock import _patch +from unittest.mock import MagicMock, Mock, _patch, patch +import ifaddr import pytest +from . import LOOPBACK_IPADDR, NO_LOOPBACK_IPADDR + + +def _generate_mock_adapters(): + mock_lo0 = Mock(spec=ifaddr.Adapter) + mock_lo0.nice_name = "lo0" + mock_lo0.ips = [ifaddr.IP(LOOPBACK_IPADDR, 8, "lo0")] + mock_lo0.index = 0 + mock_eth0 = Mock(spec=ifaddr.Adapter) + mock_eth0.nice_name = "eth0" + mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth0.index = 1 + mock_eth1 = Mock(spec=ifaddr.Adapter) + mock_eth1.nice_name = "eth1" + mock_eth1.ips = [ifaddr.IP(NO_LOOPBACK_IPADDR, 23, "eth1")] + mock_eth1.index = 2 + mock_vtun0 = Mock(spec=ifaddr.Adapter) + mock_vtun0.nice_name = "vtun0" + mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] + mock_vtun0.index = 3 + return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] + + +def _mock_socket(sockname: list[str]) -> Generator[None]: + """Mock the network socket.""" + with patch( + "homeassistant.components.network.util.socket.socket", + return_value=MagicMock(getsockname=Mock(return_value=sockname)), + ): + yield + @pytest.fixture(autouse=True) -def mock_network(): +def mock_network() -> Generator[None]: """Override mock of network util's async_get_adapters.""" + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + yield @pytest.fixture(autouse=True) @@ -19,3 +56,21 @@ def override_mock_get_source_ip( mock_get_source_ip.stop() yield mock_get_source_ip.start() + + +@pytest.fixture +def mock_socket(request: pytest.FixtureRequest) -> Generator[None]: + """Mock the network socket.""" + yield from _mock_socket(request.param) + + +@pytest.fixture +def mock_socket_loopback() -> Generator[None]: + """Mock the network socket with loopback address.""" + yield from _mock_socket([LOOPBACK_IPADDR]) + + +@pytest.fixture +def mock_socket_no_loopback() -> Generator[None]: + """Mock the network socket with loopback address.""" + yield from _mock_socket([NO_LOOPBACK_IPADDR]) diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index dca31106dba..a2352e6af9e 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -4,7 +4,6 @@ from ipaddress import IPv4Address from typing import Any from unittest.mock import MagicMock, Mock, patch -import ifaddr import pytest from homeassistant.components import network @@ -20,17 +19,10 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from . import LOOPBACK_IPADDR, NO_LOOPBACK_IPADDR + from tests.typing import WebSocketGenerator -_NO_LOOPBACK_IPADDR = "192.168.1.5" -_LOOPBACK_IPADDR = "127.0.0.1" - - -def _mock_socket(sockname): - mock_socket = MagicMock() - mock_socket.getsockname = Mock(return_value=sockname) - return mock_socket - def _mock_cond_socket(sockname): class CondMockSock(MagicMock): @@ -54,42 +46,13 @@ def _mock_socket_exception(exc): return mock_socket -def _generate_mock_adapters(): - mock_lo0 = Mock(spec=ifaddr.Adapter) - mock_lo0.nice_name = "lo0" - mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] - mock_lo0.index = 0 - mock_eth0 = Mock(spec=ifaddr.Adapter) - mock_eth0.nice_name = "eth0" - mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] - mock_eth0.index = 1 - mock_eth1 = Mock(spec=ifaddr.Adapter) - mock_eth1.nice_name = "eth1" - mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")] - mock_eth1.index = 2 - mock_vtun0 = Mock(spec=ifaddr.Adapter) - mock_vtun0.nice_name = "vtun0" - mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] - mock_vtun0.index = 3 - return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] - - +@pytest.mark.usefixtures("mock_socket_no_loopback") async def test_async_detect_interfaces_setting_non_loopback_route( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route returns a non-loopback address.""" - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] @@ -141,22 +104,13 @@ async def test_async_detect_interfaces_setting_non_loopback_route( ] +@pytest.mark.usefixtures("mock_socket_loopback") async def test_async_detect_interfaces_setting_loopback_route( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route returns a loopback address.""" - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_LOOPBACK_IPADDR]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] @@ -207,22 +161,14 @@ async def test_async_detect_interfaces_setting_loopback_route( ] +@pytest.mark.parametrize("mock_socket", [[]], indirect=True) +@pytest.mark.usefixtures("mock_socket") async def test_async_detect_interfaces_setting_empty_route( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route returns nothing.""" - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == [] @@ -277,15 +223,9 @@ async def test_async_detect_interfaces_setting_exception( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route throws an exception.""" - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket_exception(AttributeError), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), + with patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket_exception(AttributeError), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -339,6 +279,7 @@ async def test_async_detect_interfaces_setting_exception( ] +@pytest.mark.usefixtures("mock_socket_no_loopback") async def test_interfaces_configured_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -348,18 +289,9 @@ async def test_interfaces_configured_from_storage( "key": STORAGE_KEY, "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, } - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() network_obj = hass.data[DOMAIN] assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] @@ -422,15 +354,9 @@ async def test_interfaces_configured_from_storage_websocket_update( "key": STORAGE_KEY, "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, } - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), + with patch( + "homeassistant.components.network.util.socket.socket", + return_value=MagicMock(getsockname=Mock(return_value=[NO_LOOPBACK_IPADDR])), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -546,6 +472,7 @@ async def test_interfaces_configured_from_storage_websocket_update( ] +@pytest.mark.usefixtures("mock_socket_no_loopback") async def test_async_get_source_ip_matching_interface( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -556,22 +483,13 @@ async def test_async_get_source_ip_matching_interface( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with ( - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() - assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == NO_LOOPBACK_IPADDR +@pytest.mark.usefixtures("mock_socket_no_loopback") async def test_async_get_source_ip_interface_not_match( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -582,22 +500,14 @@ async def test_async_get_source_ip_interface_not_match( "data": {ATTR_CONFIGURED_ADAPTERS: ["vtun0"]}, } - with ( - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() - assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "169.254.3.2" + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "169.254.3.2" +@pytest.mark.parametrize("mock_socket", [[None]], indirect=True) +@pytest.mark.usefixtures("mock_socket") async def test_async_get_source_ip_cannot_determine_target( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -608,22 +518,13 @@ async def test_async_get_source_ip_cannot_determine_target( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with ( - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([None]), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() - assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" + assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == NO_LOOPBACK_IPADDR +@pytest.mark.usefixtures("mock_socket_no_loopback") async def test_async_get_ipv4_broadcast_addresses_default( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -634,24 +535,15 @@ async def test_async_get_ipv4_broadcast_addresses_default( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() assert await network.async_get_ipv4_broadcast_addresses(hass) == { IPv4Address("255.255.255.255") } +@pytest.mark.usefixtures("mock_socket_loopback") async def test_async_get_ipv4_broadcast_addresses_multiple( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -662,18 +554,8 @@ async def test_async_get_ipv4_broadcast_addresses_multiple( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1", "vtun0"]}, } - with ( - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_LOOPBACK_IPADDR]), - ), - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), - ): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() assert await network.async_get_ipv4_broadcast_addresses(hass) == { IPv4Address("255.255.255.255"), @@ -682,6 +564,7 @@ async def test_async_get_ipv4_broadcast_addresses_multiple( } +@pytest.mark.usefixtures("mock_socket_no_loopback") async def test_async_get_source_ip_no_enabled_addresses( hass: HomeAssistant, hass_storage: dict[str, Any], caplog: pytest.LogCaptureFixture ) -> None: @@ -692,24 +575,23 @@ async def test_async_get_source_ip_no_enabled_addresses( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with ( - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=[], - ), - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), - ), + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert await network.async_get_source_ip(hass, MDNS_TARGET_IP) == "192.168.1.5" + assert ( + await network.async_get_source_ip(hass, MDNS_TARGET_IP) + == NO_LOOPBACK_IPADDR + ) assert "source address detection may be inaccurate" in caplog.text +@pytest.mark.parametrize("mock_socket", [[None]], indirect=True) +@pytest.mark.usefixtures("mock_socket") async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses( hass: HomeAssistant, hass_storage: dict[str, Any], caplog: pytest.LogCaptureFixture ) -> None: @@ -720,15 +602,9 @@ async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with ( - patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=[], - ), - patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([None]), - ), + with patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], ): assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -753,7 +629,7 @@ async def test_async_get_source_ip_no_ip_loopback( ), patch( "homeassistant.components.network.util.socket.socket", - return_value=_mock_cond_socket(_LOOPBACK_IPADDR), + return_value=_mock_cond_socket(LOOPBACK_IPADDR), ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/network/test_system_health.py b/tests/components/network/test_system_health.py new file mode 100644 index 00000000000..eb383aafde7 --- /dev/null +++ b/tests/components/network/test_system_health.py @@ -0,0 +1,32 @@ +"""Test network system health.""" + +import asyncio + +import pytest + +from homeassistant.components.network.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info + + +@pytest.mark.usefixtures("mock_socket_no_loopback") +async def test_network_system_health(hass: HomeAssistant) -> None: + """Test network system health.""" + + assert await async_setup_component(hass, "system_health", {}) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + info = await get_system_health_info(hass, "network") + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == { + "adapters": "eth0 (disabled), lo0 (disabled), eth1 (enabled, default, auto), vtun0 (disabled)", + "announce_addresses": "192.168.1.5", + "ipv4_addresses": "eth0 (), lo0 (127.0.0.1/8), eth1 (192.168.1.5/23), vtun0 (169.254.3.2/16)", + "ipv6_addresses": "eth0 (2001:db8::/8), lo0 (), eth1 (), vtun0 ()", + } diff --git a/tests/components/nextcloud/snapshots/test_binary_sensor.ambr b/tests/components/nextcloud/snapshots/test_binary_sensor.ambr index 1831419af52..578659d411d 100644 --- a/tests/components/nextcloud/snapshots/test_binary_sensor.ambr +++ b/tests/components/nextcloud/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nextcloud/snapshots/test_sensor.ambr b/tests/components/nextcloud/snapshots/test_sensor.ambr index c49ba3496da..d01bcc112bf 100644 --- a/tests/components/nextcloud/snapshots/test_sensor.ambr +++ b/tests/components/nextcloud/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -57,6 +58,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -106,6 +108,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -155,6 +158,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -204,6 +208,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -253,6 +258,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -302,6 +308,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -351,6 +358,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -400,6 +408,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -449,6 +458,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -498,6 +508,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -547,6 +558,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -596,6 +608,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -645,6 +658,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -694,6 +708,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -743,6 +758,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -792,6 +808,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -841,6 +858,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -890,6 +908,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -939,6 +958,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -986,6 +1006,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1032,6 +1053,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1088,6 +1110,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1137,6 +1160,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1186,6 +1210,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1235,6 +1260,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1284,6 +1310,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1331,6 +1358,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1378,6 +1406,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1424,6 +1453,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1474,6 +1504,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1524,6 +1555,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1574,6 +1606,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1628,6 +1661,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1674,6 +1708,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1720,6 +1755,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1774,6 +1810,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1828,6 +1865,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1882,6 +1920,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1936,6 +1975,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1992,6 +2032,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2039,6 +2080,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2093,6 +2135,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2147,6 +2190,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2201,6 +2245,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2247,6 +2292,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2293,6 +2339,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2341,6 +2388,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2391,6 +2439,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2440,6 +2489,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2489,6 +2539,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2536,6 +2587,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2586,6 +2638,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2642,6 +2695,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2689,6 +2743,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2741,6 +2796,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2788,6 +2844,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2837,6 +2894,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2886,6 +2944,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2935,6 +2994,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2984,6 +3044,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3031,6 +3092,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3078,6 +3140,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3132,6 +3195,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3186,6 +3250,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3234,6 +3299,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3288,6 +3354,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3342,6 +3409,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3388,6 +3456,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3444,6 +3513,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3491,6 +3561,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3545,6 +3616,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3591,6 +3663,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3637,6 +3710,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3683,6 +3757,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3729,6 +3804,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3775,6 +3851,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3829,6 +3906,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3885,6 +3963,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3932,6 +4011,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nextcloud/snapshots/test_update.ambr b/tests/components/nextcloud/snapshots/test_update.ambr index 484106580b1..a8acd2f5294 100644 --- a/tests/components/nextcloud/snapshots/test_update.ambr +++ b/tests/components/nextcloud/snapshots/test_update.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nextdns/snapshots/test_binary_sensor.ambr b/tests/components/nextdns/snapshots/test_binary_sensor.ambr index 814b4c1ac16..65a477f50f3 100644 --- a/tests/components/nextdns/snapshots/test_binary_sensor.ambr +++ b/tests/components/nextdns/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nextdns/snapshots/test_button.ambr b/tests/components/nextdns/snapshots/test_button.ambr index 32dc31eea19..3f1f75d1783 100644 --- a/tests/components/nextdns/snapshots/test_button.ambr +++ b/tests/components/nextdns/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nextdns/snapshots/test_sensor.ambr b/tests/components/nextdns/snapshots/test_sensor.ambr index 14bebea53f8..48c3b0894db 100644 --- a/tests/components/nextdns/snapshots/test_sensor.ambr +++ b/tests/components/nextdns/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -108,6 +110,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -158,6 +161,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -208,6 +212,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -258,6 +263,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -308,6 +314,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -358,6 +365,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -408,6 +416,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -458,6 +467,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -508,6 +518,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -558,6 +569,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -608,6 +620,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -658,6 +671,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -708,6 +722,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -758,6 +773,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -808,6 +824,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -858,6 +875,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -908,6 +926,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -958,6 +977,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1008,6 +1028,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1058,6 +1079,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1108,6 +1130,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1158,6 +1181,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1208,6 +1232,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nextdns/snapshots/test_switch.ambr b/tests/components/nextdns/snapshots/test_switch.ambr index 3328e341a2e..e6d63b7f542 100644 --- a/tests/components/nextdns/snapshots/test_switch.ambr +++ b/tests/components/nextdns/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -282,6 +288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -328,6 +335,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -374,6 +382,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -420,6 +429,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -466,6 +476,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -512,6 +523,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -558,6 +570,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -604,6 +617,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -650,6 +664,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -696,6 +711,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -742,6 +758,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -788,6 +805,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -834,6 +852,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -880,6 +899,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -926,6 +946,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -972,6 +993,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1018,6 +1040,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1064,6 +1087,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1110,6 +1134,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1156,6 +1181,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1202,6 +1228,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1248,6 +1275,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1294,6 +1322,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1340,6 +1369,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1386,6 +1416,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1432,6 +1463,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1478,6 +1510,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1524,6 +1557,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1570,6 +1604,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1616,6 +1651,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1662,6 +1698,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1708,6 +1745,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1754,6 +1792,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1800,6 +1839,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1846,6 +1886,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1892,6 +1933,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1938,6 +1980,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1984,6 +2027,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2030,6 +2074,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2076,6 +2121,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2122,6 +2168,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2168,6 +2215,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2214,6 +2262,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2260,6 +2309,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2306,6 +2356,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2352,6 +2403,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2398,6 +2450,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2444,6 +2497,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2490,6 +2544,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2536,6 +2591,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2582,6 +2638,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2628,6 +2685,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2674,6 +2732,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2720,6 +2779,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2766,6 +2826,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2812,6 +2873,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2858,6 +2920,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2904,6 +2967,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2950,6 +3014,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2996,6 +3061,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3042,6 +3108,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3088,6 +3155,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3134,6 +3202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3180,6 +3249,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3226,6 +3296,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3272,6 +3343,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3318,6 +3390,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nice_go/snapshots/test_cover.ambr b/tests/components/nice_go/snapshots/test_cover.ambr index 49b5267df56..0e1f9013a94 100644 --- a/tests/components/nice_go/snapshots/test_cover.ambr +++ b/tests/components/nice_go/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nice_go/snapshots/test_light.ambr b/tests/components/nice_go/snapshots/test_light.ambr index 529df95a570..2b88b7d8d74 100644 --- a/tests/components/nice_go/snapshots/test_light.ambr +++ b/tests/components/nice_go/snapshots/test_light.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nice_go/test_cover.py b/tests/components/nice_go/test_cover.py index f90c2d438b0..542b1717d88 100644 --- a/tests/components/nice_go/test_cover.py +++ b/tests/components/nice_go/test_cover.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory -from nice_go import ApiError +from nice_go import ApiError, AuthFailedError import pytest from syrupy import SnapshotAssertion @@ -154,3 +154,86 @@ async def test_cover_exceptions( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + +async def test_auth_failed_error( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that if an auth failed error occurs, the integration attempts a token refresh and a retry before throwing an error.""" + + await setup_integration(hass, mock_config_entry, [Platform.COVER]) + + def _open_side_effect(*args, **kwargs): + if mock_nice_go.open_barrier.call_count <= 3: + raise AuthFailedError + if mock_nice_go.open_barrier.call_count == 5: + raise AuthFailedError + if mock_nice_go.open_barrier.call_count == 6: + raise ApiError + + def _close_side_effect(*args, **kwargs): + if mock_nice_go.close_barrier.call_count <= 3: + raise AuthFailedError + if mock_nice_go.close_barrier.call_count == 4: + raise ApiError + + mock_nice_go.open_barrier.side_effect = _open_side_effect + mock_nice_go.close_barrier.side_effect = _close_side_effect + + with pytest.raises(HomeAssistantError, match="Error opening the barrier"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_1"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 1 + assert mock_nice_go.open_barrier.call_count == 2 + + with pytest.raises(HomeAssistantError, match="Error closing the barrier"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_2"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 2 + assert mock_nice_go.close_barrier.call_count == 2 + + # Try again, but this time the auth failed error should not be raised + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_1"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 3 + assert mock_nice_go.open_barrier.call_count == 4 + + # One more time but with an ApiError instead of AuthFailed + + with pytest.raises(HomeAssistantError, match="Error opening the barrier"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_1"}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError, match="Error closing the barrier"): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_garage_2"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 5 + assert mock_nice_go.open_barrier.call_count == 6 + assert mock_nice_go.close_barrier.call_count == 4 diff --git a/tests/components/nice_go/test_light.py b/tests/components/nice_go/test_light.py index b170a0ee3ab..2bc9de59b2b 100644 --- a/tests/components/nice_go/test_light.py +++ b/tests/components/nice_go/test_light.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from aiohttp import ClientError -from nice_go import ApiError +from nice_go import ApiError, AuthFailedError import pytest from syrupy import SnapshotAssertion @@ -160,3 +160,86 @@ async def test_unsupported_device_type( "Please create an issue with your device model in additional info" in caplog.text ) + + +async def test_auth_failed_error( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that if an auth failed error occurs, the integration attempts a token refresh and a retry before throwing an error.""" + + await setup_integration(hass, mock_config_entry, [Platform.LIGHT]) + + def _on_side_effect(*args, **kwargs): + if mock_nice_go.light_on.call_count <= 3: + raise AuthFailedError + if mock_nice_go.light_on.call_count == 5: + raise AuthFailedError + if mock_nice_go.light_on.call_count == 6: + raise ApiError + + def _off_side_effect(*args, **kwargs): + if mock_nice_go.light_off.call_count <= 3: + raise AuthFailedError + if mock_nice_go.light_off.call_count == 4: + raise ApiError + + mock_nice_go.light_on.side_effect = _on_side_effect + mock_nice_go.light_off.side_effect = _off_side_effect + + with pytest.raises(HomeAssistantError, match="Error while turning on the light"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_garage_1_light"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 1 + assert mock_nice_go.light_on.call_count == 2 + + with pytest.raises(HomeAssistantError, match="Error while turning off the light"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_garage_2_light"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 2 + assert mock_nice_go.light_off.call_count == 2 + + # Try again, but this time the auth failed error should not be raised + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_garage_1_light"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 3 + assert mock_nice_go.light_on.call_count == 4 + + # One more time but with an ApiError instead of AuthFailed + + with pytest.raises(HomeAssistantError, match="Error while turning on the light"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_garage_1_light"}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError, match="Error while turning off the light"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_garage_2_light"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 5 + assert mock_nice_go.light_on.call_count == 6 + assert mock_nice_go.light_off.call_count == 4 diff --git a/tests/components/nice_go/test_switch.py b/tests/components/nice_go/test_switch.py index d3a2141eb2b..cab009c5b94 100644 --- a/tests/components/nice_go/test_switch.py +++ b/tests/components/nice_go/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from aiohttp import ClientError -from nice_go import ApiError +from nice_go import ApiError, AuthFailedError import pytest from homeassistant.components.switch import ( @@ -88,3 +88,86 @@ async def test_error( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + +async def test_auth_failed_error( + hass: HomeAssistant, + mock_nice_go: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that if an auth failed error occurs, the integration attempts a token refresh and a retry before throwing an error.""" + + await setup_integration(hass, mock_config_entry, [Platform.SWITCH]) + + def _on_side_effect(*args, **kwargs): + if mock_nice_go.vacation_mode_on.call_count <= 3: + raise AuthFailedError + if mock_nice_go.vacation_mode_on.call_count == 5: + raise AuthFailedError + if mock_nice_go.vacation_mode_on.call_count == 6: + raise ApiError + + def _off_side_effect(*args, **kwargs): + if mock_nice_go.vacation_mode_off.call_count <= 3: + raise AuthFailedError + if mock_nice_go.vacation_mode_off.call_count == 4: + raise ApiError + + mock_nice_go.vacation_mode_on.side_effect = _on_side_effect + mock_nice_go.vacation_mode_off.side_effect = _off_side_effect + + with pytest.raises(HomeAssistantError, match="Error while turning on the switch"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_garage_1_vacation_mode"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 1 + assert mock_nice_go.vacation_mode_on.call_count == 2 + + with pytest.raises(HomeAssistantError, match="Error while turning off the switch"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_garage_2_vacation_mode"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 2 + assert mock_nice_go.vacation_mode_off.call_count == 2 + + # Try again, but this time the auth failed error should not be raised + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_garage_1_vacation_mode"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 3 + assert mock_nice_go.vacation_mode_on.call_count == 4 + + # One more time but with an ApiError instead of AuthFailed + + with pytest.raises(HomeAssistantError, match="Error while turning on the switch"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_garage_1_vacation_mode"}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError, match="Error while turning off the switch"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_garage_2_vacation_mode"}, + blocking=True, + ) + + assert mock_nice_go.authenticate.call_count == 5 + assert mock_nice_go.vacation_mode_on.call_count == 6 + assert mock_nice_go.vacation_mode_off.call_count == 4 diff --git a/tests/components/niko_home_control/__init__.py b/tests/components/niko_home_control/__init__.py index f6e8187bf0f..0182a24ba7c 100644 --- a/tests/components/niko_home_control/__init__.py +++ b/tests/components/niko_home_control/__init__.py @@ -1,5 +1,10 @@ """Tests for the niko_home_control integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import AsyncMock + +import pytest + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -11,3 +16,13 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + + +def find_update_callback( + mock: AsyncMock, identifier: int +) -> Callable[[int], Awaitable[None]]: + """Find the update callback for a specific identifier.""" + for call in mock.register_callback.call_args_list: + if call[0][0] == identifier: + return call[0][1] + pytest.fail(f"Callback for identifier {identifier} not found") diff --git a/tests/components/niko_home_control/conftest.py b/tests/components/niko_home_control/conftest.py index 932480ac710..130baf72228 100644 --- a/tests/components/niko_home_control/conftest.py +++ b/tests/components/niko_home_control/conftest.py @@ -3,6 +3,8 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch +from nhc.cover import NHCCover +from nhc.light import NHCLight import pytest from homeassistant.components.niko_home_control.const import DOMAIN @@ -22,16 +24,61 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_niko_home_control_connection() -> Generator[AsyncMock]: +def light() -> NHCLight: + """Return a light mock.""" + mock = AsyncMock(spec=NHCLight) + mock.id = 1 + mock.type = 1 + mock.is_dimmable = False + mock.name = "light" + mock.suggested_area = "room" + mock.state = 100 + return mock + + +@pytest.fixture +def dimmable_light() -> NHCLight: + """Return a dimmable light mock.""" + mock = AsyncMock(spec=NHCLight) + mock.id = 2 + mock.type = 2 + mock.is_dimmable = True + mock.name = "dimmable light" + mock.suggested_area = "room" + mock.state = 100 + return mock + + +@pytest.fixture +def cover() -> NHCCover: + """Return a cover mock.""" + mock = AsyncMock(spec=NHCCover) + mock.id = 3 + mock.type = 4 + mock.name = "cover" + mock.suggested_area = "room" + mock.state = 100 + return mock + + +@pytest.fixture +def mock_niko_home_control_connection( + light: NHCLight, dimmable_light: NHCLight, cover: NHCCover +) -> Generator[AsyncMock]: """Mock a NHC client.""" with ( patch( - "homeassistant.components.niko_home_control.config_flow.NikoHomeControlConnection", + "homeassistant.components.niko_home_control.NHCController", autospec=True, ) as mock_client, + patch( + "homeassistant.components.niko_home_control.config_flow.NHCController", + new=mock_client, + ), ): client = mock_client.return_value - client.return_value = True + client.lights = [light, dimmable_light] + client.covers = [cover] yield client @@ -39,5 +86,8 @@ def mock_niko_home_control_connection() -> Generator[AsyncMock]: def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" return MockConfigEntry( - domain=DOMAIN, title="Niko Home Control", data={CONF_HOST: "192.168.0.123"} + domain=DOMAIN, + title="Niko Home Control", + data={CONF_HOST: "192.168.0.123"}, + entry_id="01JFN93M7KRA38V5AMPCJ2JYYV", ) diff --git a/tests/components/niko_home_control/snapshots/test_cover.ambr b/tests/components/niko_home_control/snapshots/test_cover.ambr new file mode 100644 index 00000000000..5fe89497298 --- /dev/null +++ b/tests/components/niko_home_control/snapshots/test_cover.ambr @@ -0,0 +1,49 @@ +# serializer version: 1 +# name: test_cover[cover.cover-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.cover', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'niko_home_control', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover[cover.cover-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'cover', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.cover', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/niko_home_control/snapshots/test_light.ambr b/tests/components/niko_home_control/snapshots/test_light.ambr new file mode 100644 index 00000000000..adb0e743786 --- /dev/null +++ b/tests/components/niko_home_control/snapshots/test_light.ambr @@ -0,0 +1,114 @@ +# serializer version: 1 +# name: test_entities[light.dimmable_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmable_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'niko_home_control', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[light.dimmable_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 255, + 'color_mode': , + 'friendly_name': 'dimmable light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmable_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[light.light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'niko_home_control', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[light.light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/niko_home_control/test_config_flow.py b/tests/components/niko_home_control/test_config_flow.py index 8220ee15e02..f911f4ebb1a 100644 --- a/tests/components/niko_home_control/test_config_flow.py +++ b/tests/components/niko_home_control/test_config_flow.py @@ -46,7 +46,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_setup_entry: AsyncMock) assert result["errors"] == {} with patch( - "homeassistant.components.niko_home_control.config_flow.NikoHomeControlConnection", + "homeassistant.components.niko_home_control.config_flow.NHCController.connect", side_effect=Exception, ): result = await hass.config_entries.flow.async_configure( @@ -58,7 +58,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_setup_entry: AsyncMock) assert result["errors"] == {"base": "cannot_connect"} with patch( - "homeassistant.components.niko_home_control.config_flow.NikoHomeControlConnection" + "homeassistant.components.niko_home_control.config_flow.NHCController.connect", ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -114,7 +114,7 @@ async def test_import_cannot_connect( """Test the cannot connect error.""" with patch( - "homeassistant.components.niko_home_control.config_flow.NikoHomeControlConnection", + "homeassistant.components.niko_home_control.config_flow.NHCController.connect", side_effect=Exception, ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/niko_home_control/test_cover.py b/tests/components/niko_home_control/test_cover.py new file mode 100644 index 00000000000..5e9a17c3324 --- /dev/null +++ b/tests/components/niko_home_control/test_cover.py @@ -0,0 +1,138 @@ +"""Tests for the Niko Home Control cover platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_OPEN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import find_update_callback, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_cover( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.niko_home_control.PLATFORMS", [Platform.COVER] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("cover_id", "entity_id"), + [ + (0, "cover.cover"), + ], +) +async def test_open_cover( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover_id: int, + entity_id: int, +) -> None: + """Test opening the cover.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.covers[cover_id].open.assert_called_once_with() + + +@pytest.mark.parametrize( + ("cover_id", "entity_id"), + [ + (0, "cover.cover"), + ], +) +async def test_close_cover( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover_id: int, + entity_id: str, +) -> None: + """Test closing the cover.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.covers[cover_id].close.assert_called_once_with() + + +@pytest.mark.parametrize( + ("cover_id", "entity_id"), + [ + (0, "cover.cover"), + ], +) +async def test_stop_cover( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover_id: int, + entity_id: str, +) -> None: + """Test closing the cover.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.covers[cover_id].stop.assert_called_once_with() + + +async def test_updating( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + cover: AsyncMock, +) -> None: + """Test closing the cover.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("cover.cover").state == STATE_OPEN + + cover.state = 0 + await find_update_callback(mock_niko_home_control_connection, 3)(0) + await hass.async_block_till_done() + + assert hass.states.get("cover.cover").state == STATE_CLOSED + + cover.state = 100 + await find_update_callback(mock_niko_home_control_connection, 3)(100) + await hass.async_block_till_done() + + assert hass.states.get("cover.cover").state == STATE_OPEN diff --git a/tests/components/niko_home_control/test_init.py b/tests/components/niko_home_control/test_init.py new file mode 100644 index 00000000000..422b7d7c30c --- /dev/null +++ b/tests/components/niko_home_control/test_init.py @@ -0,0 +1,36 @@ +"""Test init.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.niko_home_control.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_migrate_entry( + hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_setup_entry: AsyncMock +) -> None: + """Validate that the unique_id is migrated to the new unique_id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + minor_version=1, + data={CONF_HOST: "192.168.0.123"}, + ) + config_entry.add_to_hass(hass) + entity_entry = entity_registry.async_get_or_create( + LIGHT_DOMAIN, DOMAIN, "light-1", config_entry=config_entry + ) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(entity_entry.entity_id) + + assert config_entry.minor_version == 2 + assert ( + entity_registry.async_get(entity_entry.entity_id).unique_id + == f"{config_entry.entry_id}-1" + ) diff --git a/tests/components/niko_home_control/test_light.py b/tests/components/niko_home_control/test_light.py new file mode 100644 index 00000000000..865e1303cb0 --- /dev/null +++ b/tests/components/niko_home_control/test_light.py @@ -0,0 +1,136 @@ +"""Tests for the Niko Home Control Light platform.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import find_update_callback, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.niko_home_control.PLATFORMS", [Platform.LIGHT] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("light_id", "data", "set_brightness"), + [ + (0, {ATTR_ENTITY_ID: "light.light"}, 100), + ( + 1, + {ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 50}, + 20, + ), + ], +) +async def test_turning_on( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + light_id: int, + data: dict[str, Any], + set_brightness: int, +) -> None: + """Test turning on the light.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + data, + blocking=True, + ) + mock_niko_home_control_connection.lights[light_id].turn_on.assert_called_once_with( + set_brightness + ) + + +@pytest.mark.parametrize( + ("light_id", "entity_id"), + [ + (0, "light.light"), + (1, "light.dimmable_light"), + ], +) +async def test_turning_off( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + light_id: int, + entity_id: str, +) -> None: + """Test turning on the light.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_niko_home_control_connection.lights[ + light_id + ].turn_off.assert_called_once_with() + + +async def test_updating( + hass: HomeAssistant, + mock_niko_home_control_connection: AsyncMock, + mock_config_entry: MockConfigEntry, + light: AsyncMock, + dimmable_light: AsyncMock, +) -> None: + """Test turning on the light.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("light.light").state == STATE_ON + + light.state = 0 + await find_update_callback(mock_niko_home_control_connection, 1)(0) + await hass.async_block_till_done() + + assert hass.states.get("light.light").state == STATE_OFF + + assert hass.states.get("light.dimmable_light").state == STATE_ON + assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 255 + + dimmable_light.state = 80 + await find_update_callback(mock_niko_home_control_connection, 2)(80) + await hass.async_block_till_done() + + assert hass.states.get("light.dimmable_light").state == STATE_ON + assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 204 + + dimmable_light.state = 0 + await find_update_callback(mock_niko_home_control_connection, 2)(0) + await hass.async_block_till_done() + + assert hass.states.get("light.dimmable_light").state == STATE_OFF + assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] is None diff --git a/tests/components/nmbs/__init__.py b/tests/components/nmbs/__init__.py new file mode 100644 index 00000000000..91226950aba --- /dev/null +++ b/tests/components/nmbs/__init__.py @@ -0,0 +1,20 @@ +"""Tests for the NMBS integration.""" + +import json +from typing import Any + +from tests.common import load_fixture + + +def mock_api_unavailable() -> dict[str, Any]: + """Mock for unavailable api.""" + return -1 + + +def mock_station_response() -> dict[str, Any]: + """Mock for valid station response.""" + dummy_stations_response: dict[str, Any] = json.loads( + load_fixture("stations.json", "nmbs") + ) + + return dummy_stations_response diff --git a/tests/components/nmbs/conftest.py b/tests/components/nmbs/conftest.py new file mode 100644 index 00000000000..69200fc4c98 --- /dev/null +++ b/tests/components/nmbs/conftest.py @@ -0,0 +1,58 @@ +"""NMBS tests configuration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.nmbs.const import ( + CONF_STATION_FROM, + CONF_STATION_TO, + DOMAIN, +) + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.nmbs.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_nmbs_client() -> Generator[AsyncMock]: + """Mock a NMBS client.""" + with ( + patch( + "homeassistant.components.nmbs.iRail", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.nmbs.config_flow.iRail", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_stations.return_value = load_json_object_fixture( + "stations.json", DOMAIN + ) + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Train from Brussel-Noord/Bruxelles-Nord to Brussel-Zuid/Bruxelles-Midi", + data={ + CONF_STATION_FROM: "BE.NMBS.008812005", + CONF_STATION_TO: "BE.NMBS.008814001", + }, + unique_id="BE.NMBS.008812005_BE.NMBS.008814001", + ) diff --git a/tests/components/nmbs/fixtures/stations.json b/tests/components/nmbs/fixtures/stations.json new file mode 100644 index 00000000000..b774e064f78 --- /dev/null +++ b/tests/components/nmbs/fixtures/stations.json @@ -0,0 +1,30 @@ +{ + "version": "1.3", + "timestamp": "1720252400", + "station": [ + { + "@id": "http://irail.be/stations/NMBS/008812005", + "id": "BE.NMBS.008812005", + "name": "Brussels-North", + "locationX": "4.360846", + "locationY": "50.859663", + "standardname": "Brussel-Noord/Bruxelles-Nord" + }, + { + "@id": "http://irail.be/stations/NMBS/008813003", + "id": "BE.NMBS.008813003", + "name": "Brussels-Central", + "locationX": "4.356801", + "locationY": "50.845658", + "standardname": "Brussel-Centraal/Bruxelles-Central" + }, + { + "@id": "http://irail.be/stations/NMBS/008814001", + "id": "BE.NMBS.008814001", + "name": "Brussels-South/Brussels-Midi", + "locationX": "4.336531", + "locationY": "50.835707", + "standardname": "Brussel-Zuid/Bruxelles-Midi" + } + ] +} diff --git a/tests/components/nmbs/test_config_flow.py b/tests/components/nmbs/test_config_flow.py new file mode 100644 index 00000000000..ff4c5bdf72a --- /dev/null +++ b/tests/components/nmbs/test_config_flow.py @@ -0,0 +1,341 @@ +"""Test the NMBS config flow.""" + +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from homeassistant import config_entries +from homeassistant.components.nmbs.config_flow import CONF_EXCLUDE_VIAS +from homeassistant.components.nmbs.const import ( + CONF_STATION_FROM, + CONF_STATION_LIVE, + CONF_STATION_TO, + DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +DUMMY_DATA_IMPORT: dict[str, Any] = { + "STAT_BRUSSELS_NORTH": "Brussel-Noord/Bruxelles-Nord", + "STAT_BRUSSELS_CENTRAL": "Brussel-Centraal/Bruxelles-Central", + "STAT_BRUSSELS_SOUTH": "Brussel-Zuid/Bruxelles-Midi", +} + +DUMMY_DATA_ALTERNATIVE_IMPORT: dict[str, Any] = { + "STAT_BRUSSELS_NORTH": "Brussels-North", + "STAT_BRUSSELS_CENTRAL": "Brussels-Central", + "STAT_BRUSSELS_SOUTH": "Brussels-South/Brussels-Midi", +} + +DUMMY_DATA: dict[str, Any] = { + "STAT_BRUSSELS_NORTH": "BE.NMBS.008812005", + "STAT_BRUSSELS_CENTRAL": "BE.NMBS.008813003", + "STAT_BRUSSELS_SOUTH": "BE.NMBS.008814001", +} + + +async def test_full_flow( + hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test the full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"], + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert ( + result["title"] + == "Train from Brussel-Noord/Bruxelles-Nord to Brussel-Zuid/Bruxelles-Midi" + ) + assert result["data"] == { + CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"], + } + assert ( + result["result"].unique_id + == f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}" + ) + + +async def test_same_station( + hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test selecting the same station.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "same_station"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"], + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_abort_if_exists( + hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test aborting the flow if the entry already exists.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"], + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_dont_abort_if_exists_when_vias_differs( + hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test aborting the flow if the entry already exists.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"], + CONF_EXCLUDE_VIAS: True, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_unavailable_api( + hass: HomeAssistant, mock_nmbs_client: AsyncMock +) -> None: + """Test starting a flow by user and api is unavailable.""" + mock_nmbs_client.get_stations.return_value = -1 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "api_unavailable" + + +async def test_import( + hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test starting a flow by user which filled in data for connection.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_LIVE: DUMMY_DATA_IMPORT["STAT_BRUSSELS_CENTRAL"], + CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"], + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert ( + result["title"] + == "Train from Brussel-Noord/Bruxelles-Nord to Brussel-Zuid/Bruxelles-Midi" + ) + assert result["data"] == { + CONF_STATION_FROM: "BE.NMBS.008812005", + CONF_STATION_LIVE: "BE.NMBS.008813003", + CONF_STATION_TO: "BE.NMBS.008814001", + } + assert ( + result["result"].unique_id + == f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}" + ) + + +async def test_step_import_abort_if_already_setup( + hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_config_entry: MockConfigEntry +) -> None: + """Test starting a flow by user which filled in data for connection for already existing connection.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"], + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_unavailable_api_import( + hass: HomeAssistant, mock_nmbs_client: AsyncMock +) -> None: + """Test starting a flow by import and api is unavailable.""" + mock_nmbs_client.get_stations.return_value = -1 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_LIVE: DUMMY_DATA_IMPORT["STAT_BRUSSELS_CENTRAL"], + CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"], + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "api_unavailable" + + +@pytest.mark.parametrize( + ("config", "reason"), + [ + ( + { + CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: "Utrecht Centraal", + }, + "invalid_station", + ), + ( + { + CONF_STATION_FROM: "Utrecht Centraal", + CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"], + }, + "invalid_station", + ), + ( + { + CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + }, + "same_station", + ), + ], +) +async def test_invalid_station_name( + hass: HomeAssistant, + mock_nmbs_client: AsyncMock, + config: dict[str, Any], + reason: str, +) -> None: + """Test importing invalid YAML.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == reason + + +async def test_sensor_id_migration_standardname( + hass: HomeAssistant, + mock_nmbs_client: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating unique id.""" + old_unique_id = ( + f"live_{DUMMY_DATA_IMPORT['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA_IMPORT['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA_IMPORT['STAT_BRUSSELS_SOUTH']}" + ) + new_unique_id = ( + f"nmbs_live_{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}" + ) + old_entry = entity_registry.async_get_or_create( + SENSOR_DOMAIN, DOMAIN, old_unique_id + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_STATION_LIVE: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"], + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + config_entry_id = result["result"].entry_id + await hass.async_block_till_done() + entities = er.async_entries_for_config_entry(entity_registry, config_entry_id) + assert len(entities) == 3 + entities_map = {entity.unique_id: entity for entity in entities} + assert old_unique_id not in entities_map + assert new_unique_id in entities_map + assert entities_map[new_unique_id].id == old_entry.id + + +async def test_sensor_id_migration_localized_name( + hass: HomeAssistant, + mock_nmbs_client: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrating unique id.""" + old_unique_id = ( + f"live_{DUMMY_DATA_ALTERNATIVE_IMPORT['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA_ALTERNATIVE_IMPORT['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA_ALTERNATIVE_IMPORT['STAT_BRUSSELS_SOUTH']}" + ) + new_unique_id = ( + f"nmbs_live_{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_" + f"{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}" + ) + old_entry = entity_registry.async_get_or_create( + SENSOR_DOMAIN, DOMAIN, old_unique_id + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_STATION_LIVE: DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_FROM: DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_NORTH"], + CONF_STATION_TO: DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_SOUTH"], + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + config_entry_id = result["result"].entry_id + await hass.async_block_till_done() + entities = er.async_entries_for_config_entry(entity_registry, config_entry_id) + assert len(entities) == 3 + entities_map = {entity.unique_id: entity for entity in entities} + assert old_unique_id not in entities_map + assert new_unique_id in entities_map + assert entities_map[new_unique_id].id == old_entry.id diff --git a/tests/components/nordpool/conftest.py b/tests/components/nordpool/conftest.py index 9b7ab4b2afa..ca1e2a05a0b 100644 --- a/tests/components/nordpool/conftest.py +++ b/tests/components/nordpool/conftest.py @@ -3,20 +3,15 @@ from __future__ import annotations from collections.abc import AsyncGenerator -from datetime import datetime import json from typing import Any -from unittest.mock import patch -from pynordpool import NordPoolClient -from pynordpool.const import Currency -from pynordpool.model import DeliveryPeriodData +from pynordpool import API, NordPoolClient import pytest from homeassistant.components.nordpool.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import ENTRY_CONFIG @@ -24,17 +19,8 @@ from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -@pytest.fixture(autouse=True) -async def no_sleep() -> AsyncGenerator[None]: - """No sleeping.""" - with patch("homeassistant.components.nordpool.coordinator.asyncio.sleep"): - yield - - @pytest.fixture -async def load_int( - hass: HomeAssistant, get_data: DeliveryPeriodData -) -> MockConfigEntry: +async def load_int(hass: HomeAssistant, get_client: NordPoolClient) -> MockConfigEntry: """Set up the Nord Pool integration in Home Assistant.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -44,40 +30,83 @@ async def load_int( config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry -@pytest.fixture(name="get_data") +@pytest.fixture(name="get_client") async def get_data_from_library( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, load_json: dict[str, Any] -) -> DeliveryPeriodData: + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + load_json: list[dict[str, Any]], +) -> AsyncGenerator[NordPoolClient]: """Retrieve data from Nord Pool library.""" - + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-05", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=load_json[0], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-05", + "market": "DayAhead", + "deliveryArea": "SE3", + "currency": "EUR", + }, + json=load_json[0], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-04", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=load_json[1], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-06", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=load_json[2], + ) client = NordPoolClient(aioclient_mock.create_session(hass.loop)) - with patch("pynordpool.NordPoolClient._get", return_value=load_json): - output = await client.async_get_delivery_period( - datetime(2024, 11, 5, 13, tzinfo=dt_util.UTC), Currency.SEK, ["SE3", "SE4"] - ) + yield client await client._session.close() - return output @pytest.fixture(name="load_json") -def load_json_from_fixture(load_data: str) -> dict[str, Any]: +def load_json_from_fixture(load_data: list[str, str, str]) -> list[dict[str, Any]]: """Load fixture with json data and return.""" - return json.loads(load_data) + return [ + json.loads(load_data[0]), + json.loads(load_data[1]), + json.loads(load_data[2]), + ] @pytest.fixture(name="load_data", scope="package") -def load_data_from_fixture() -> str: +def load_data_from_fixture() -> list[str, str, str]: """Load fixture with fixture data and return.""" - return load_fixture("delivery_period.json", DOMAIN) + return [ + load_fixture("delivery_period_today.json", DOMAIN), + load_fixture("delivery_period_yesterday.json", DOMAIN), + load_fixture("delivery_period_tomorrow.json", DOMAIN), + ] diff --git a/tests/components/nordpool/fixtures/delivery_period_nl.json b/tests/components/nordpool/fixtures/delivery_period_nl.json new file mode 100644 index 00000000000..cd326e05d01 --- /dev/null +++ b/tests/components/nordpool/fixtures/delivery_period_nl.json @@ -0,0 +1,229 @@ +{ + "deliveryDateCET": "2024-11-05", + "version": 2, + "updatedAt": "2024-11-04T11:58:10.7711584Z", + "deliveryAreas": ["NL"], + "market": "DayAhead", + "multiAreaEntries": [ + { + "deliveryStart": "2024-11-04T23:00:00Z", + "deliveryEnd": "2024-11-05T00:00:00Z", + "entryPerArea": { + "NL": 83.63 + } + }, + { + "deliveryStart": "2024-11-05T00:00:00Z", + "deliveryEnd": "2024-11-05T01:00:00Z", + "entryPerArea": { + "NL": 94.0 + } + }, + { + "deliveryStart": "2024-11-05T01:00:00Z", + "deliveryEnd": "2024-11-05T02:00:00Z", + "entryPerArea": { + "NL": 90.68 + } + }, + { + "deliveryStart": "2024-11-05T02:00:00Z", + "deliveryEnd": "2024-11-05T03:00:00Z", + "entryPerArea": { + "NL": 91.3 + } + }, + { + "deliveryStart": "2024-11-05T03:00:00Z", + "deliveryEnd": "2024-11-05T04:00:00Z", + "entryPerArea": { + "NL": 94.0 + } + }, + { + "deliveryStart": "2024-11-05T04:00:00Z", + "deliveryEnd": "2024-11-05T05:00:00Z", + "entryPerArea": { + "NL": 96.09 + } + }, + { + "deliveryStart": "2024-11-05T05:00:00Z", + "deliveryEnd": "2024-11-05T06:00:00Z", + "entryPerArea": { + "NL": 106.0 + } + }, + { + "deliveryStart": "2024-11-05T06:00:00Z", + "deliveryEnd": "2024-11-05T07:00:00Z", + "entryPerArea": { + "NL": 135.99 + } + }, + { + "deliveryStart": "2024-11-05T07:00:00Z", + "deliveryEnd": "2024-11-05T08:00:00Z", + "entryPerArea": { + "NL": 136.21 + } + }, + { + "deliveryStart": "2024-11-05T08:00:00Z", + "deliveryEnd": "2024-11-05T09:00:00Z", + "entryPerArea": { + "NL": 118.23 + } + }, + { + "deliveryStart": "2024-11-05T09:00:00Z", + "deliveryEnd": "2024-11-05T10:00:00Z", + "entryPerArea": { + "NL": 105.87 + } + }, + { + "deliveryStart": "2024-11-05T10:00:00Z", + "deliveryEnd": "2024-11-05T11:00:00Z", + "entryPerArea": { + "NL": 95.28 + } + }, + { + "deliveryStart": "2024-11-05T11:00:00Z", + "deliveryEnd": "2024-11-05T12:00:00Z", + "entryPerArea": { + "NL": 94.92 + } + }, + { + "deliveryStart": "2024-11-05T12:00:00Z", + "deliveryEnd": "2024-11-05T13:00:00Z", + "entryPerArea": { + "NL": 99.25 + } + }, + { + "deliveryStart": "2024-11-05T13:00:00Z", + "deliveryEnd": "2024-11-05T14:00:00Z", + "entryPerArea": { + "NL": 107.98 + } + }, + { + "deliveryStart": "2024-11-05T14:00:00Z", + "deliveryEnd": "2024-11-05T15:00:00Z", + "entryPerArea": { + "NL": 149.86 + } + }, + { + "deliveryStart": "2024-11-05T15:00:00Z", + "deliveryEnd": "2024-11-05T16:00:00Z", + "entryPerArea": { + "NL": 303.24 + } + }, + { + "deliveryStart": "2024-11-05T16:00:00Z", + "deliveryEnd": "2024-11-05T17:00:00Z", + "entryPerArea": { + "NL": 472.99 + } + }, + { + "deliveryStart": "2024-11-05T17:00:00Z", + "deliveryEnd": "2024-11-05T18:00:00Z", + "entryPerArea": { + "NL": 431.02 + } + }, + { + "deliveryStart": "2024-11-05T18:00:00Z", + "deliveryEnd": "2024-11-05T19:00:00Z", + "entryPerArea": { + "NL": 320.33 + } + }, + { + "deliveryStart": "2024-11-05T19:00:00Z", + "deliveryEnd": "2024-11-05T20:00:00Z", + "entryPerArea": { + "NL": 169.7 + } + }, + { + "deliveryStart": "2024-11-05T20:00:00Z", + "deliveryEnd": "2024-11-05T21:00:00Z", + "entryPerArea": { + "NL": 129.9 + } + }, + { + "deliveryStart": "2024-11-05T21:00:00Z", + "deliveryEnd": "2024-11-05T22:00:00Z", + "entryPerArea": { + "NL": 117.77 + } + }, + { + "deliveryStart": "2024-11-05T22:00:00Z", + "deliveryEnd": "2024-11-05T23:00:00Z", + "entryPerArea": { + "NL": 110.03 + } + } + ], + "blockPriceAggregates": [ + { + "blockName": "Off-peak 1", + "deliveryStart": "2024-11-04T23:00:00Z", + "deliveryEnd": "2024-11-05T07:00:00Z", + "averagePricePerArea": { + "NL": { + "average": 98.96, + "min": 83.63, + "max": 135.99 + } + } + }, + { + "blockName": "Peak", + "deliveryStart": "2024-11-05T07:00:00Z", + "deliveryEnd": "2024-11-05T19:00:00Z", + "averagePricePerArea": { + "NL": { + "average": 202.93, + "min": 94.92, + "max": 472.99 + } + } + }, + { + "blockName": "Off-peak 2", + "deliveryStart": "2024-11-05T19:00:00Z", + "deliveryEnd": "2024-11-05T23:00:00Z", + "averagePricePerArea": { + "NL": { + "average": 131.85, + "min": 110.03, + "max": 169.7 + } + } + } + ], + "currency": "EUR", + "exchangeRate": 1, + "areaStates": [ + { + "state": "Final", + "areas": ["NL"] + } + ], + "areaAverages": [ + { + "areaCode": "NL", + "price": 156.43 + } + ] +} diff --git a/tests/components/nordpool/fixtures/delivery_period.json b/tests/components/nordpool/fixtures/delivery_period_today.json similarity index 100% rename from tests/components/nordpool/fixtures/delivery_period.json rename to tests/components/nordpool/fixtures/delivery_period_today.json diff --git a/tests/components/nordpool/fixtures/delivery_period_tomorrow.json b/tests/components/nordpool/fixtures/delivery_period_tomorrow.json new file mode 100644 index 00000000000..abaa24e93ed --- /dev/null +++ b/tests/components/nordpool/fixtures/delivery_period_tomorrow.json @@ -0,0 +1,272 @@ +{ + "deliveryDateCET": "2024-11-06", + "version": 3, + "updatedAt": "2024-11-05T12:12:51.9853434Z", + "deliveryAreas": ["SE3", "SE4"], + "market": "DayAhead", + "multiAreaEntries": [ + { + "deliveryStart": "2024-11-05T23:00:00Z", + "deliveryEnd": "2024-11-06T00:00:00Z", + "entryPerArea": { + "SE3": 126.66, + "SE4": 275.6 + } + }, + { + "deliveryStart": "2024-11-06T00:00:00Z", + "deliveryEnd": "2024-11-06T01:00:00Z", + "entryPerArea": { + "SE3": 74.06, + "SE4": 157.34 + } + }, + { + "deliveryStart": "2024-11-06T01:00:00Z", + "deliveryEnd": "2024-11-06T02:00:00Z", + "entryPerArea": { + "SE3": 78.38, + "SE4": 165.62 + } + }, + { + "deliveryStart": "2024-11-06T02:00:00Z", + "deliveryEnd": "2024-11-06T03:00:00Z", + "entryPerArea": { + "SE3": 92.37, + "SE4": 196.17 + } + }, + { + "deliveryStart": "2024-11-06T03:00:00Z", + "deliveryEnd": "2024-11-06T04:00:00Z", + "entryPerArea": { + "SE3": 99.14, + "SE4": 190.58 + } + }, + { + "deliveryStart": "2024-11-06T04:00:00Z", + "deliveryEnd": "2024-11-06T05:00:00Z", + "entryPerArea": { + "SE3": 447.51, + "SE4": 932.93 + } + }, + { + "deliveryStart": "2024-11-06T05:00:00Z", + "deliveryEnd": "2024-11-06T06:00:00Z", + "entryPerArea": { + "SE3": 641.47, + "SE4": 1284.69 + } + }, + { + "deliveryStart": "2024-11-06T06:00:00Z", + "deliveryEnd": "2024-11-06T07:00:00Z", + "entryPerArea": { + "SE3": 1820.5, + "SE4": 2449.96 + } + }, + { + "deliveryStart": "2024-11-06T07:00:00Z", + "deliveryEnd": "2024-11-06T08:00:00Z", + "entryPerArea": { + "SE3": 1723.0, + "SE4": 2244.22 + } + }, + { + "deliveryStart": "2024-11-06T08:00:00Z", + "deliveryEnd": "2024-11-06T09:00:00Z", + "entryPerArea": { + "SE3": 1298.57, + "SE4": 1643.45 + } + }, + { + "deliveryStart": "2024-11-06T09:00:00Z", + "deliveryEnd": "2024-11-06T10:00:00Z", + "entryPerArea": { + "SE3": 1099.25, + "SE4": 1507.23 + } + }, + { + "deliveryStart": "2024-11-06T10:00:00Z", + "deliveryEnd": "2024-11-06T11:00:00Z", + "entryPerArea": { + "SE3": 903.31, + "SE4": 1362.84 + } + }, + { + "deliveryStart": "2024-11-06T11:00:00Z", + "deliveryEnd": "2024-11-06T12:00:00Z", + "entryPerArea": { + "SE3": 959.99, + "SE4": 1376.13 + } + }, + { + "deliveryStart": "2024-11-06T12:00:00Z", + "deliveryEnd": "2024-11-06T13:00:00Z", + "entryPerArea": { + "SE3": 1186.61, + "SE4": 1449.96 + } + }, + { + "deliveryStart": "2024-11-06T13:00:00Z", + "deliveryEnd": "2024-11-06T14:00:00Z", + "entryPerArea": { + "SE3": 1307.67, + "SE4": 1608.35 + } + }, + { + "deliveryStart": "2024-11-06T14:00:00Z", + "deliveryEnd": "2024-11-06T15:00:00Z", + "entryPerArea": { + "SE3": 1385.46, + "SE4": 2110.8 + } + }, + { + "deliveryStart": "2024-11-06T15:00:00Z", + "deliveryEnd": "2024-11-06T16:00:00Z", + "entryPerArea": { + "SE3": 1366.8, + "SE4": 3031.25 + } + }, + { + "deliveryStart": "2024-11-06T16:00:00Z", + "deliveryEnd": "2024-11-06T17:00:00Z", + "entryPerArea": { + "SE3": 2366.57, + "SE4": 5511.77 + } + }, + { + "deliveryStart": "2024-11-06T17:00:00Z", + "deliveryEnd": "2024-11-06T18:00:00Z", + "entryPerArea": { + "SE3": 1481.92, + "SE4": 3351.64 + } + }, + { + "deliveryStart": "2024-11-06T18:00:00Z", + "deliveryEnd": "2024-11-06T19:00:00Z", + "entryPerArea": { + "SE3": 1082.69, + "SE4": 2484.95 + } + }, + { + "deliveryStart": "2024-11-06T19:00:00Z", + "deliveryEnd": "2024-11-06T20:00:00Z", + "entryPerArea": { + "SE3": 716.82, + "SE4": 1624.33 + } + }, + { + "deliveryStart": "2024-11-06T20:00:00Z", + "deliveryEnd": "2024-11-06T21:00:00Z", + "entryPerArea": { + "SE3": 583.16, + "SE4": 1306.27 + } + }, + { + "deliveryStart": "2024-11-06T21:00:00Z", + "deliveryEnd": "2024-11-06T22:00:00Z", + "entryPerArea": { + "SE3": 523.09, + "SE4": 1142.99 + } + }, + { + "deliveryStart": "2024-11-06T22:00:00Z", + "deliveryEnd": "2024-11-06T23:00:00Z", + "entryPerArea": { + "SE3": 250.64, + "SE4": 539.42 + } + } + ], + "blockPriceAggregates": [ + { + "blockName": "Off-peak 1", + "deliveryStart": "2024-11-05T23:00:00Z", + "deliveryEnd": "2024-11-06T07:00:00Z", + "averagePricePerArea": { + "SE3": { + "average": 422.51, + "min": 74.06, + "max": 1820.5 + }, + "SE4": { + "average": 706.61, + "min": 157.34, + "max": 2449.96 + } + } + }, + { + "blockName": "Peak", + "deliveryStart": "2024-11-06T07:00:00Z", + "deliveryEnd": "2024-11-06T19:00:00Z", + "averagePricePerArea": { + "SE3": { + "average": 1346.82, + "min": 903.31, + "max": 2366.57 + }, + "SE4": { + "average": 2306.88, + "min": 1362.84, + "max": 5511.77 + } + } + }, + { + "blockName": "Off-peak 2", + "deliveryStart": "2024-11-06T19:00:00Z", + "deliveryEnd": "2024-11-06T23:00:00Z", + "averagePricePerArea": { + "SE3": { + "average": 518.43, + "min": 250.64, + "max": 716.82 + }, + "SE4": { + "average": 1153.25, + "min": 539.42, + "max": 1624.33 + } + } + } + ], + "currency": "SEK", + "exchangeRate": 11.66314, + "areaStates": [ + { + "state": "Final", + "areas": ["SE3", "SE4"] + } + ], + "areaAverages": [ + { + "areaCode": "SE3", + "price": 900.65 + }, + { + "areaCode": "SE4", + "price": 1581.19 + } + ] +} diff --git a/tests/components/nordpool/fixtures/delivery_period_yesterday.json b/tests/components/nordpool/fixtures/delivery_period_yesterday.json new file mode 100644 index 00000000000..bc79aeb99f0 --- /dev/null +++ b/tests/components/nordpool/fixtures/delivery_period_yesterday.json @@ -0,0 +1,272 @@ +{ + "deliveryDateCET": "2024-11-04", + "version": 3, + "updatedAt": "2024-11-04T08:09:11.1931991Z", + "deliveryAreas": ["SE3", "SE4"], + "market": "DayAhead", + "multiAreaEntries": [ + { + "deliveryStart": "2024-11-03T23:00:00Z", + "deliveryEnd": "2024-11-04T00:00:00Z", + "entryPerArea": { + "SE3": 66.13, + "SE4": 78.59 + } + }, + { + "deliveryStart": "2024-11-04T00:00:00Z", + "deliveryEnd": "2024-11-04T01:00:00Z", + "entryPerArea": { + "SE3": 72.54, + "SE4": 86.51 + } + }, + { + "deliveryStart": "2024-11-04T01:00:00Z", + "deliveryEnd": "2024-11-04T02:00:00Z", + "entryPerArea": { + "SE3": 73.12, + "SE4": 84.88 + } + }, + { + "deliveryStart": "2024-11-04T02:00:00Z", + "deliveryEnd": "2024-11-04T03:00:00Z", + "entryPerArea": { + "SE3": 171.97, + "SE4": 217.26 + } + }, + { + "deliveryStart": "2024-11-04T03:00:00Z", + "deliveryEnd": "2024-11-04T04:00:00Z", + "entryPerArea": { + "SE3": 181.05, + "SE4": 227.74 + } + }, + { + "deliveryStart": "2024-11-04T04:00:00Z", + "deliveryEnd": "2024-11-04T05:00:00Z", + "entryPerArea": { + "SE3": 360.71, + "SE4": 414.61 + } + }, + { + "deliveryStart": "2024-11-04T05:00:00Z", + "deliveryEnd": "2024-11-04T06:00:00Z", + "entryPerArea": { + "SE3": 917.83, + "SE4": 1439.33 + } + }, + { + "deliveryStart": "2024-11-04T06:00:00Z", + "deliveryEnd": "2024-11-04T07:00:00Z", + "entryPerArea": { + "SE3": 1426.17, + "SE4": 1695.95 + } + }, + { + "deliveryStart": "2024-11-04T07:00:00Z", + "deliveryEnd": "2024-11-04T08:00:00Z", + "entryPerArea": { + "SE3": 1350.96, + "SE4": 1605.13 + } + }, + { + "deliveryStart": "2024-11-04T08:00:00Z", + "deliveryEnd": "2024-11-04T09:00:00Z", + "entryPerArea": { + "SE3": 1195.06, + "SE4": 1393.46 + } + }, + { + "deliveryStart": "2024-11-04T09:00:00Z", + "deliveryEnd": "2024-11-04T10:00:00Z", + "entryPerArea": { + "SE3": 992.35, + "SE4": 1126.71 + } + }, + { + "deliveryStart": "2024-11-04T10:00:00Z", + "deliveryEnd": "2024-11-04T11:00:00Z", + "entryPerArea": { + "SE3": 976.63, + "SE4": 1107.97 + } + }, + { + "deliveryStart": "2024-11-04T11:00:00Z", + "deliveryEnd": "2024-11-04T12:00:00Z", + "entryPerArea": { + "SE3": 952.76, + "SE4": 1085.73 + } + }, + { + "deliveryStart": "2024-11-04T12:00:00Z", + "deliveryEnd": "2024-11-04T13:00:00Z", + "entryPerArea": { + "SE3": 1029.37, + "SE4": 1177.71 + } + }, + { + "deliveryStart": "2024-11-04T13:00:00Z", + "deliveryEnd": "2024-11-04T14:00:00Z", + "entryPerArea": { + "SE3": 1043.35, + "SE4": 1194.59 + } + }, + { + "deliveryStart": "2024-11-04T14:00:00Z", + "deliveryEnd": "2024-11-04T15:00:00Z", + "entryPerArea": { + "SE3": 1359.57, + "SE4": 1561.12 + } + }, + { + "deliveryStart": "2024-11-04T15:00:00Z", + "deliveryEnd": "2024-11-04T16:00:00Z", + "entryPerArea": { + "SE3": 1848.35, + "SE4": 2145.84 + } + }, + { + "deliveryStart": "2024-11-04T16:00:00Z", + "deliveryEnd": "2024-11-04T17:00:00Z", + "entryPerArea": { + "SE3": 2812.53, + "SE4": 3313.53 + } + }, + { + "deliveryStart": "2024-11-04T17:00:00Z", + "deliveryEnd": "2024-11-04T18:00:00Z", + "entryPerArea": { + "SE3": 2351.69, + "SE4": 2751.87 + } + }, + { + "deliveryStart": "2024-11-04T18:00:00Z", + "deliveryEnd": "2024-11-04T19:00:00Z", + "entryPerArea": { + "SE3": 1553.08, + "SE4": 1842.77 + } + }, + { + "deliveryStart": "2024-11-04T19:00:00Z", + "deliveryEnd": "2024-11-04T20:00:00Z", + "entryPerArea": { + "SE3": 1165.02, + "SE4": 1398.35 + } + }, + { + "deliveryStart": "2024-11-04T20:00:00Z", + "deliveryEnd": "2024-11-04T21:00:00Z", + "entryPerArea": { + "SE3": 1007.48, + "SE4": 1172.35 + } + }, + { + "deliveryStart": "2024-11-04T21:00:00Z", + "deliveryEnd": "2024-11-04T22:00:00Z", + "entryPerArea": { + "SE3": 792.09, + "SE4": 920.28 + } + }, + { + "deliveryStart": "2024-11-04T22:00:00Z", + "deliveryEnd": "2024-11-04T23:00:00Z", + "entryPerArea": { + "SE3": 465.38, + "SE4": 528.83 + } + } + ], + "blockPriceAggregates": [ + { + "blockName": "Off-peak 1", + "deliveryStart": "2024-11-03T23:00:00Z", + "deliveryEnd": "2024-11-04T07:00:00Z", + "averagePricePerArea": { + "SE3": { + "average": 408.69, + "min": 66.13, + "max": 1426.17 + }, + "SE4": { + "average": 530.61, + "min": 78.59, + "max": 1695.95 + } + } + }, + { + "blockName": "Peak", + "deliveryStart": "2024-11-04T07:00:00Z", + "deliveryEnd": "2024-11-04T19:00:00Z", + "averagePricePerArea": { + "SE3": { + "average": 1455.48, + "min": 952.76, + "max": 2812.53 + }, + "SE4": { + "average": 1692.2, + "min": 1085.73, + "max": 3313.53 + } + } + }, + { + "blockName": "Off-peak 2", + "deliveryStart": "2024-11-04T19:00:00Z", + "deliveryEnd": "2024-11-04T23:00:00Z", + "averagePricePerArea": { + "SE3": { + "average": 857.49, + "min": 465.38, + "max": 1165.02 + }, + "SE4": { + "average": 1004.95, + "min": 528.83, + "max": 1398.35 + } + } + } + ], + "currency": "SEK", + "exchangeRate": 11.64318, + "areaStates": [ + { + "state": "Final", + "areas": ["SE3", "SE4"] + } + ], + "areaAverages": [ + { + "areaCode": "SE3", + "price": 1006.88 + }, + { + "areaCode": "SE4", + "price": 1190.46 + } + ] +} diff --git a/tests/components/nordpool/snapshots/test_diagnostics.ambr b/tests/components/nordpool/snapshots/test_diagnostics.ambr index dde2eca0022..76a3dd96405 100644 --- a/tests/components/nordpool/snapshots/test_diagnostics.ambr +++ b/tests/components/nordpool/snapshots/test_diagnostics.ambr @@ -2,282 +2,840 @@ # name: test_diagnostics dict({ 'raw': dict({ - 'areaAverages': list([ - dict({ - 'areaCode': 'SE3', - 'price': 900.74, - }), - dict({ - 'areaCode': 'SE4', - 'price': 1166.12, - }), - ]), - 'areaStates': list([ - dict({ - 'areas': list([ - 'SE3', - 'SE4', - ]), - 'state': 'Final', - }), - ]), - 'blockPriceAggregates': list([ - dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 422.87, - 'max': 1406.14, - 'min': 61.69, + '2024-11-04': dict({ + 'areaAverages': list([ + dict({ + 'areaCode': 'SE3', + 'price': 1006.88, + }), + dict({ + 'areaCode': 'SE4', + 'price': 1190.46, + }), + ]), + 'areaStates': list([ + dict({ + 'areas': list([ + 'SE3', + 'SE4', + ]), + 'state': 'Final', + }), + ]), + 'blockPriceAggregates': list([ + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 408.69, + 'max': 1426.17, + 'min': 66.13, + }), + 'SE4': dict({ + 'average': 530.61, + 'max': 1695.95, + 'min': 78.59, + }), }), - 'SE4': dict({ - 'average': 497.97, - 'max': 1648.25, - 'min': 65.19, + 'blockName': 'Off-peak 1', + 'deliveryEnd': '2024-11-04T07:00:00Z', + 'deliveryStart': '2024-11-03T23:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 1455.48, + 'max': 2812.53, + 'min': 952.76, + }), + 'SE4': dict({ + 'average': 1692.2, + 'max': 3313.53, + 'min': 1085.73, + }), + }), + 'blockName': 'Peak', + 'deliveryEnd': '2024-11-04T19:00:00Z', + 'deliveryStart': '2024-11-04T07:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 857.49, + 'max': 1165.02, + 'min': 465.38, + }), + 'SE4': dict({ + 'average': 1004.95, + 'max': 1398.35, + 'min': 528.83, + }), + }), + 'blockName': 'Off-peak 2', + 'deliveryEnd': '2024-11-04T23:00:00Z', + 'deliveryStart': '2024-11-04T19:00:00Z', + }), + ]), + 'currency': 'SEK', + 'deliveryAreas': list([ + 'SE3', + 'SE4', + ]), + 'deliveryDateCET': '2024-11-04', + 'exchangeRate': 11.64318, + 'market': 'DayAhead', + 'multiAreaEntries': list([ + dict({ + 'deliveryEnd': '2024-11-04T00:00:00Z', + 'deliveryStart': '2024-11-03T23:00:00Z', + 'entryPerArea': dict({ + 'SE3': 66.13, + 'SE4': 78.59, }), }), - 'blockName': 'Off-peak 1', - 'deliveryEnd': '2024-11-05T07:00:00Z', - 'deliveryStart': '2024-11-04T23:00:00Z', - }), - dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 1315.97, - 'max': 2512.65, - 'min': 925.05, - }), - 'SE4': dict({ - 'average': 1735.59, - 'max': 3533.03, - 'min': 1081.72, + dict({ + 'deliveryEnd': '2024-11-04T01:00:00Z', + 'deliveryStart': '2024-11-04T00:00:00Z', + 'entryPerArea': dict({ + 'SE3': 72.54, + 'SE4': 86.51, }), }), - 'blockName': 'Peak', - 'deliveryEnd': '2024-11-05T19:00:00Z', - 'deliveryStart': '2024-11-05T07:00:00Z', - }), - dict({ - 'averagePricePerArea': dict({ - 'SE3': dict({ - 'average': 610.79, - 'max': 835.53, - 'min': 289.14, - }), - 'SE4': dict({ - 'average': 793.98, - 'max': 1112.57, - 'min': 349.21, + dict({ + 'deliveryEnd': '2024-11-04T02:00:00Z', + 'deliveryStart': '2024-11-04T01:00:00Z', + 'entryPerArea': dict({ + 'SE3': 73.12, + 'SE4': 84.88, }), }), - 'blockName': 'Off-peak 2', - 'deliveryEnd': '2024-11-05T23:00:00Z', - 'deliveryStart': '2024-11-05T19:00:00Z', - }), - ]), - 'currency': 'SEK', - 'deliveryAreas': list([ - 'SE3', - 'SE4', - ]), - 'deliveryDateCET': '2024-11-05', - 'exchangeRate': 11.6402, - 'market': 'DayAhead', - 'multiAreaEntries': list([ - dict({ - 'deliveryEnd': '2024-11-05T00:00:00Z', - 'deliveryStart': '2024-11-04T23:00:00Z', - 'entryPerArea': dict({ - 'SE3': 250.73, - 'SE4': 283.79, + dict({ + 'deliveryEnd': '2024-11-04T03:00:00Z', + 'deliveryStart': '2024-11-04T02:00:00Z', + 'entryPerArea': dict({ + 'SE3': 171.97, + 'SE4': 217.26, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T01:00:00Z', - 'deliveryStart': '2024-11-05T00:00:00Z', - 'entryPerArea': dict({ - 'SE3': 76.36, - 'SE4': 81.36, + dict({ + 'deliveryEnd': '2024-11-04T04:00:00Z', + 'deliveryStart': '2024-11-04T03:00:00Z', + 'entryPerArea': dict({ + 'SE3': 181.05, + 'SE4': 227.74, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T02:00:00Z', - 'deliveryStart': '2024-11-05T01:00:00Z', - 'entryPerArea': dict({ - 'SE3': 73.92, - 'SE4': 79.15, + dict({ + 'deliveryEnd': '2024-11-04T05:00:00Z', + 'deliveryStart': '2024-11-04T04:00:00Z', + 'entryPerArea': dict({ + 'SE3': 360.71, + 'SE4': 414.61, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T03:00:00Z', - 'deliveryStart': '2024-11-05T02:00:00Z', - 'entryPerArea': dict({ - 'SE3': 61.69, - 'SE4': 65.19, + dict({ + 'deliveryEnd': '2024-11-04T06:00:00Z', + 'deliveryStart': '2024-11-04T05:00:00Z', + 'entryPerArea': dict({ + 'SE3': 917.83, + 'SE4': 1439.33, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T04:00:00Z', - 'deliveryStart': '2024-11-05T03:00:00Z', - 'entryPerArea': dict({ - 'SE3': 64.6, - 'SE4': 68.44, + dict({ + 'deliveryEnd': '2024-11-04T07:00:00Z', + 'deliveryStart': '2024-11-04T06:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1426.17, + 'SE4': 1695.95, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T05:00:00Z', - 'deliveryStart': '2024-11-05T04:00:00Z', - 'entryPerArea': dict({ - 'SE3': 453.27, - 'SE4': 516.71, + dict({ + 'deliveryEnd': '2024-11-04T08:00:00Z', + 'deliveryStart': '2024-11-04T07:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1350.96, + 'SE4': 1605.13, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T06:00:00Z', - 'deliveryStart': '2024-11-05T05:00:00Z', - 'entryPerArea': dict({ - 'SE3': 996.28, - 'SE4': 1240.85, + dict({ + 'deliveryEnd': '2024-11-04T09:00:00Z', + 'deliveryStart': '2024-11-04T08:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1195.06, + 'SE4': 1393.46, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T07:00:00Z', - 'deliveryStart': '2024-11-05T06:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1406.14, - 'SE4': 1648.25, + dict({ + 'deliveryEnd': '2024-11-04T10:00:00Z', + 'deliveryStart': '2024-11-04T09:00:00Z', + 'entryPerArea': dict({ + 'SE3': 992.35, + 'SE4': 1126.71, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T08:00:00Z', - 'deliveryStart': '2024-11-05T07:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1346.54, - 'SE4': 1570.5, + dict({ + 'deliveryEnd': '2024-11-04T11:00:00Z', + 'deliveryStart': '2024-11-04T10:00:00Z', + 'entryPerArea': dict({ + 'SE3': 976.63, + 'SE4': 1107.97, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T09:00:00Z', - 'deliveryStart': '2024-11-05T08:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1150.28, - 'SE4': 1345.37, + dict({ + 'deliveryEnd': '2024-11-04T12:00:00Z', + 'deliveryStart': '2024-11-04T11:00:00Z', + 'entryPerArea': dict({ + 'SE3': 952.76, + 'SE4': 1085.73, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T10:00:00Z', - 'deliveryStart': '2024-11-05T09:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1031.32, - 'SE4': 1206.51, + dict({ + 'deliveryEnd': '2024-11-04T13:00:00Z', + 'deliveryStart': '2024-11-04T12:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1029.37, + 'SE4': 1177.71, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T11:00:00Z', - 'deliveryStart': '2024-11-05T10:00:00Z', - 'entryPerArea': dict({ - 'SE3': 927.37, - 'SE4': 1085.8, + dict({ + 'deliveryEnd': '2024-11-04T14:00:00Z', + 'deliveryStart': '2024-11-04T13:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1043.35, + 'SE4': 1194.59, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T12:00:00Z', - 'deliveryStart': '2024-11-05T11:00:00Z', - 'entryPerArea': dict({ - 'SE3': 925.05, - 'SE4': 1081.72, + dict({ + 'deliveryEnd': '2024-11-04T15:00:00Z', + 'deliveryStart': '2024-11-04T14:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1359.57, + 'SE4': 1561.12, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T13:00:00Z', - 'deliveryStart': '2024-11-05T12:00:00Z', - 'entryPerArea': dict({ - 'SE3': 949.49, - 'SE4': 1130.38, + dict({ + 'deliveryEnd': '2024-11-04T16:00:00Z', + 'deliveryStart': '2024-11-04T15:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1848.35, + 'SE4': 2145.84, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T14:00:00Z', - 'deliveryStart': '2024-11-05T13:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1042.03, - 'SE4': 1256.91, + dict({ + 'deliveryEnd': '2024-11-04T17:00:00Z', + 'deliveryStart': '2024-11-04T16:00:00Z', + 'entryPerArea': dict({ + 'SE3': 2812.53, + 'SE4': 3313.53, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T15:00:00Z', - 'deliveryStart': '2024-11-05T14:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1258.89, - 'SE4': 1765.82, + dict({ + 'deliveryEnd': '2024-11-04T18:00:00Z', + 'deliveryStart': '2024-11-04T17:00:00Z', + 'entryPerArea': dict({ + 'SE3': 2351.69, + 'SE4': 2751.87, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T16:00:00Z', - 'deliveryStart': '2024-11-05T15:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1816.45, - 'SE4': 2522.55, + dict({ + 'deliveryEnd': '2024-11-04T19:00:00Z', + 'deliveryStart': '2024-11-04T18:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1553.08, + 'SE4': 1842.77, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T17:00:00Z', - 'deliveryStart': '2024-11-05T16:00:00Z', - 'entryPerArea': dict({ - 'SE3': 2512.65, - 'SE4': 3533.03, + dict({ + 'deliveryEnd': '2024-11-04T20:00:00Z', + 'deliveryStart': '2024-11-04T19:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1165.02, + 'SE4': 1398.35, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T18:00:00Z', - 'deliveryStart': '2024-11-05T17:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1819.83, - 'SE4': 2524.06, + dict({ + 'deliveryEnd': '2024-11-04T21:00:00Z', + 'deliveryStart': '2024-11-04T20:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1007.48, + 'SE4': 1172.35, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T19:00:00Z', - 'deliveryStart': '2024-11-05T18:00:00Z', - 'entryPerArea': dict({ - 'SE3': 1011.77, - 'SE4': 1804.46, + dict({ + 'deliveryEnd': '2024-11-04T22:00:00Z', + 'deliveryStart': '2024-11-04T21:00:00Z', + 'entryPerArea': dict({ + 'SE3': 792.09, + 'SE4': 920.28, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T20:00:00Z', - 'deliveryStart': '2024-11-05T19:00:00Z', - 'entryPerArea': dict({ - 'SE3': 835.53, - 'SE4': 1112.57, + dict({ + 'deliveryEnd': '2024-11-04T23:00:00Z', + 'deliveryStart': '2024-11-04T22:00:00Z', + 'entryPerArea': dict({ + 'SE3': 465.38, + 'SE4': 528.83, + }), }), - }), - dict({ - 'deliveryEnd': '2024-11-05T21:00:00Z', - 'deliveryStart': '2024-11-05T20:00:00Z', - 'entryPerArea': dict({ - 'SE3': 796.19, - 'SE4': 1051.69, + ]), + 'updatedAt': '2024-11-04T08:09:11.1931991Z', + 'version': 3, + }), + '2024-11-05': dict({ + 'areaAverages': list([ + dict({ + 'areaCode': 'SE3', + 'price': 900.74, }), - }), - dict({ - 'deliveryEnd': '2024-11-05T22:00:00Z', - 'deliveryStart': '2024-11-05T21:00:00Z', - 'entryPerArea': dict({ - 'SE3': 522.3, - 'SE4': 662.44, + dict({ + 'areaCode': 'SE4', + 'price': 1166.12, }), - }), - dict({ - 'deliveryEnd': '2024-11-05T23:00:00Z', - 'deliveryStart': '2024-11-05T22:00:00Z', - 'entryPerArea': dict({ - 'SE3': 289.14, - 'SE4': 349.21, + ]), + 'areaStates': list([ + dict({ + 'areas': list([ + 'SE3', + 'SE4', + ]), + 'state': 'Final', }), - }), - ]), - 'updatedAt': '2024-11-04T12:15:03.9456464Z', - 'version': 3, + ]), + 'blockPriceAggregates': list([ + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 422.87, + 'max': 1406.14, + 'min': 61.69, + }), + 'SE4': dict({ + 'average': 497.97, + 'max': 1648.25, + 'min': 65.19, + }), + }), + 'blockName': 'Off-peak 1', + 'deliveryEnd': '2024-11-05T07:00:00Z', + 'deliveryStart': '2024-11-04T23:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 1315.97, + 'max': 2512.65, + 'min': 925.05, + }), + 'SE4': dict({ + 'average': 1735.59, + 'max': 3533.03, + 'min': 1081.72, + }), + }), + 'blockName': 'Peak', + 'deliveryEnd': '2024-11-05T19:00:00Z', + 'deliveryStart': '2024-11-05T07:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 610.79, + 'max': 835.53, + 'min': 289.14, + }), + 'SE4': dict({ + 'average': 793.98, + 'max': 1112.57, + 'min': 349.21, + }), + }), + 'blockName': 'Off-peak 2', + 'deliveryEnd': '2024-11-05T23:00:00Z', + 'deliveryStart': '2024-11-05T19:00:00Z', + }), + ]), + 'currency': 'SEK', + 'deliveryAreas': list([ + 'SE3', + 'SE4', + ]), + 'deliveryDateCET': '2024-11-05', + 'exchangeRate': 11.6402, + 'market': 'DayAhead', + 'multiAreaEntries': list([ + dict({ + 'deliveryEnd': '2024-11-05T00:00:00Z', + 'deliveryStart': '2024-11-04T23:00:00Z', + 'entryPerArea': dict({ + 'SE3': 250.73, + 'SE4': 283.79, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T01:00:00Z', + 'deliveryStart': '2024-11-05T00:00:00Z', + 'entryPerArea': dict({ + 'SE3': 76.36, + 'SE4': 81.36, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T02:00:00Z', + 'deliveryStart': '2024-11-05T01:00:00Z', + 'entryPerArea': dict({ + 'SE3': 73.92, + 'SE4': 79.15, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T03:00:00Z', + 'deliveryStart': '2024-11-05T02:00:00Z', + 'entryPerArea': dict({ + 'SE3': 61.69, + 'SE4': 65.19, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T04:00:00Z', + 'deliveryStart': '2024-11-05T03:00:00Z', + 'entryPerArea': dict({ + 'SE3': 64.6, + 'SE4': 68.44, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T05:00:00Z', + 'deliveryStart': '2024-11-05T04:00:00Z', + 'entryPerArea': dict({ + 'SE3': 453.27, + 'SE4': 516.71, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T06:00:00Z', + 'deliveryStart': '2024-11-05T05:00:00Z', + 'entryPerArea': dict({ + 'SE3': 996.28, + 'SE4': 1240.85, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T07:00:00Z', + 'deliveryStart': '2024-11-05T06:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1406.14, + 'SE4': 1648.25, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T08:00:00Z', + 'deliveryStart': '2024-11-05T07:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1346.54, + 'SE4': 1570.5, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T09:00:00Z', + 'deliveryStart': '2024-11-05T08:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1150.28, + 'SE4': 1345.37, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T10:00:00Z', + 'deliveryStart': '2024-11-05T09:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1031.32, + 'SE4': 1206.51, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T11:00:00Z', + 'deliveryStart': '2024-11-05T10:00:00Z', + 'entryPerArea': dict({ + 'SE3': 927.37, + 'SE4': 1085.8, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T12:00:00Z', + 'deliveryStart': '2024-11-05T11:00:00Z', + 'entryPerArea': dict({ + 'SE3': 925.05, + 'SE4': 1081.72, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T13:00:00Z', + 'deliveryStart': '2024-11-05T12:00:00Z', + 'entryPerArea': dict({ + 'SE3': 949.49, + 'SE4': 1130.38, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T14:00:00Z', + 'deliveryStart': '2024-11-05T13:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1042.03, + 'SE4': 1256.91, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T15:00:00Z', + 'deliveryStart': '2024-11-05T14:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1258.89, + 'SE4': 1765.82, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T16:00:00Z', + 'deliveryStart': '2024-11-05T15:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1816.45, + 'SE4': 2522.55, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T17:00:00Z', + 'deliveryStart': '2024-11-05T16:00:00Z', + 'entryPerArea': dict({ + 'SE3': 2512.65, + 'SE4': 3533.03, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T18:00:00Z', + 'deliveryStart': '2024-11-05T17:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1819.83, + 'SE4': 2524.06, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T19:00:00Z', + 'deliveryStart': '2024-11-05T18:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1011.77, + 'SE4': 1804.46, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T20:00:00Z', + 'deliveryStart': '2024-11-05T19:00:00Z', + 'entryPerArea': dict({ + 'SE3': 835.53, + 'SE4': 1112.57, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T21:00:00Z', + 'deliveryStart': '2024-11-05T20:00:00Z', + 'entryPerArea': dict({ + 'SE3': 796.19, + 'SE4': 1051.69, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T22:00:00Z', + 'deliveryStart': '2024-11-05T21:00:00Z', + 'entryPerArea': dict({ + 'SE3': 522.3, + 'SE4': 662.44, + }), + }), + dict({ + 'deliveryEnd': '2024-11-05T23:00:00Z', + 'deliveryStart': '2024-11-05T22:00:00Z', + 'entryPerArea': dict({ + 'SE3': 289.14, + 'SE4': 349.21, + }), + }), + ]), + 'updatedAt': '2024-11-04T12:15:03.9456464Z', + 'version': 3, + }), + '2024-11-06': dict({ + 'areaAverages': list([ + dict({ + 'areaCode': 'SE3', + 'price': 900.65, + }), + dict({ + 'areaCode': 'SE4', + 'price': 1581.19, + }), + ]), + 'areaStates': list([ + dict({ + 'areas': list([ + 'SE3', + 'SE4', + ]), + 'state': 'Final', + }), + ]), + 'blockPriceAggregates': list([ + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 422.51, + 'max': 1820.5, + 'min': 74.06, + }), + 'SE4': dict({ + 'average': 706.61, + 'max': 2449.96, + 'min': 157.34, + }), + }), + 'blockName': 'Off-peak 1', + 'deliveryEnd': '2024-11-06T07:00:00Z', + 'deliveryStart': '2024-11-05T23:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 1346.82, + 'max': 2366.57, + 'min': 903.31, + }), + 'SE4': dict({ + 'average': 2306.88, + 'max': 5511.77, + 'min': 1362.84, + }), + }), + 'blockName': 'Peak', + 'deliveryEnd': '2024-11-06T19:00:00Z', + 'deliveryStart': '2024-11-06T07:00:00Z', + }), + dict({ + 'averagePricePerArea': dict({ + 'SE3': dict({ + 'average': 518.43, + 'max': 716.82, + 'min': 250.64, + }), + 'SE4': dict({ + 'average': 1153.25, + 'max': 1624.33, + 'min': 539.42, + }), + }), + 'blockName': 'Off-peak 2', + 'deliveryEnd': '2024-11-06T23:00:00Z', + 'deliveryStart': '2024-11-06T19:00:00Z', + }), + ]), + 'currency': 'SEK', + 'deliveryAreas': list([ + 'SE3', + 'SE4', + ]), + 'deliveryDateCET': '2024-11-06', + 'exchangeRate': 11.66314, + 'market': 'DayAhead', + 'multiAreaEntries': list([ + dict({ + 'deliveryEnd': '2024-11-06T00:00:00Z', + 'deliveryStart': '2024-11-05T23:00:00Z', + 'entryPerArea': dict({ + 'SE3': 126.66, + 'SE4': 275.6, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T01:00:00Z', + 'deliveryStart': '2024-11-06T00:00:00Z', + 'entryPerArea': dict({ + 'SE3': 74.06, + 'SE4': 157.34, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T02:00:00Z', + 'deliveryStart': '2024-11-06T01:00:00Z', + 'entryPerArea': dict({ + 'SE3': 78.38, + 'SE4': 165.62, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T03:00:00Z', + 'deliveryStart': '2024-11-06T02:00:00Z', + 'entryPerArea': dict({ + 'SE3': 92.37, + 'SE4': 196.17, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T04:00:00Z', + 'deliveryStart': '2024-11-06T03:00:00Z', + 'entryPerArea': dict({ + 'SE3': 99.14, + 'SE4': 190.58, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T05:00:00Z', + 'deliveryStart': '2024-11-06T04:00:00Z', + 'entryPerArea': dict({ + 'SE3': 447.51, + 'SE4': 932.93, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T06:00:00Z', + 'deliveryStart': '2024-11-06T05:00:00Z', + 'entryPerArea': dict({ + 'SE3': 641.47, + 'SE4': 1284.69, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T07:00:00Z', + 'deliveryStart': '2024-11-06T06:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1820.5, + 'SE4': 2449.96, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T08:00:00Z', + 'deliveryStart': '2024-11-06T07:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1723.0, + 'SE4': 2244.22, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T09:00:00Z', + 'deliveryStart': '2024-11-06T08:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1298.57, + 'SE4': 1643.45, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T10:00:00Z', + 'deliveryStart': '2024-11-06T09:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1099.25, + 'SE4': 1507.23, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T11:00:00Z', + 'deliveryStart': '2024-11-06T10:00:00Z', + 'entryPerArea': dict({ + 'SE3': 903.31, + 'SE4': 1362.84, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T12:00:00Z', + 'deliveryStart': '2024-11-06T11:00:00Z', + 'entryPerArea': dict({ + 'SE3': 959.99, + 'SE4': 1376.13, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T13:00:00Z', + 'deliveryStart': '2024-11-06T12:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1186.61, + 'SE4': 1449.96, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T14:00:00Z', + 'deliveryStart': '2024-11-06T13:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1307.67, + 'SE4': 1608.35, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T15:00:00Z', + 'deliveryStart': '2024-11-06T14:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1385.46, + 'SE4': 2110.8, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T16:00:00Z', + 'deliveryStart': '2024-11-06T15:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1366.8, + 'SE4': 3031.25, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T17:00:00Z', + 'deliveryStart': '2024-11-06T16:00:00Z', + 'entryPerArea': dict({ + 'SE3': 2366.57, + 'SE4': 5511.77, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T18:00:00Z', + 'deliveryStart': '2024-11-06T17:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1481.92, + 'SE4': 3351.64, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T19:00:00Z', + 'deliveryStart': '2024-11-06T18:00:00Z', + 'entryPerArea': dict({ + 'SE3': 1082.69, + 'SE4': 2484.95, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T20:00:00Z', + 'deliveryStart': '2024-11-06T19:00:00Z', + 'entryPerArea': dict({ + 'SE3': 716.82, + 'SE4': 1624.33, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T21:00:00Z', + 'deliveryStart': '2024-11-06T20:00:00Z', + 'entryPerArea': dict({ + 'SE3': 583.16, + 'SE4': 1306.27, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T22:00:00Z', + 'deliveryStart': '2024-11-06T21:00:00Z', + 'entryPerArea': dict({ + 'SE3': 523.09, + 'SE4': 1142.99, + }), + }), + dict({ + 'deliveryEnd': '2024-11-06T23:00:00Z', + 'deliveryStart': '2024-11-06T22:00:00Z', + 'entryPerArea': dict({ + 'SE3': 250.64, + 'SE4': 539.42, + }), + }), + ]), + 'updatedAt': '2024-11-05T12:12:51.9853434Z', + 'version': 3, + }), }), }) # --- diff --git a/tests/components/nordpool/snapshots/test_sensor.ambr b/tests/components/nordpool/snapshots/test_sensor.ambr index 01600352861..86aa49357c5 100644 --- a/tests/components/nordpool/snapshots/test_sensor.ambr +++ b/tests/components/nordpool/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -107,6 +109,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -160,6 +163,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -200,6 +204,59 @@ 'state': '11.6402', }) # --- +# name: test_sensor[sensor.nord_pool_se3_highest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se3_highest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Highest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'highest_price', + 'unique_id': 'SE3-highest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se3_highest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T17:00:00+00:00', + 'friendly_name': 'Nord Pool SE3 Highest price', + 'start': '2024-11-05T16:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se3_highest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.51265', + }) +# --- # name: test_sensor[sensor.nord_pool_se3_last_updated-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -207,6 +264,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -247,6 +305,59 @@ 'state': '2024-11-04T12:15:03+00:00', }) # --- +# name: test_sensor[sensor.nord_pool_se3_lowest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se3_lowest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lowest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lowest_price', + 'unique_id': 'SE3-lowest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se3_lowest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T03:00:00+00:00', + 'friendly_name': 'Nord Pool SE3 Lowest price', + 'start': '2024-11-05T02:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se3_lowest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06169', + }) +# --- # name: test_sensor[sensor.nord_pool_se3_next_price-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -254,6 +365,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -306,6 +418,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -359,6 +472,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -412,6 +526,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -463,6 +578,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -510,6 +626,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -559,6 +676,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -612,6 +730,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -665,6 +784,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -716,6 +836,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -763,6 +884,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -812,6 +934,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -865,6 +988,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -918,6 +1042,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -969,6 +1094,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1016,6 +1142,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1063,6 +1190,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1113,6 +1241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1161,6 +1290,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1214,6 +1344,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1267,6 +1398,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1307,6 +1439,59 @@ 'state': '11.6402', }) # --- +# name: test_sensor[sensor.nord_pool_se4_highest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se4_highest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Highest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'highest_price', + 'unique_id': 'SE4-highest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se4_highest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T17:00:00+00:00', + 'friendly_name': 'Nord Pool SE4 Highest price', + 'start': '2024-11-05T16:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se4_highest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.53303', + }) +# --- # name: test_sensor[sensor.nord_pool_se4_last_updated-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1314,6 +1499,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1354,6 +1540,59 @@ 'state': '2024-11-04T12:15:03+00:00', }) # --- +# name: test_sensor[sensor.nord_pool_se4_lowest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se4_lowest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lowest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lowest_price', + 'unique_id': 'SE4-lowest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se4_lowest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T03:00:00+00:00', + 'friendly_name': 'Nord Pool SE4 Lowest price', + 'start': '2024-11-05T02:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se4_lowest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06519', + }) +# --- # name: test_sensor[sensor.nord_pool_se4_next_price-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1361,6 +1600,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1413,6 +1653,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1466,6 +1707,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1519,6 +1761,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1570,6 +1813,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1617,6 +1861,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1666,6 +1911,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1719,6 +1965,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1772,6 +2019,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1823,6 +2071,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1870,6 +2119,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1919,6 +2169,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1972,6 +2223,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2025,6 +2277,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2076,6 +2329,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2123,6 +2377,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2170,6 +2425,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nordpool/snapshots/test_services.ambr b/tests/components/nordpool/snapshots/test_services.ambr new file mode 100644 index 00000000000..6a57d7ecce9 --- /dev/null +++ b/tests/components/nordpool/snapshots/test_services.ambr @@ -0,0 +1,127 @@ +# serializer version: 1 +# name: test_service_call + dict({ + 'SE3': list([ + dict({ + 'end': '2024-11-05T00:00:00+00:00', + 'price': 250.73, + 'start': '2024-11-04T23:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T01:00:00+00:00', + 'price': 76.36, + 'start': '2024-11-05T00:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T02:00:00+00:00', + 'price': 73.92, + 'start': '2024-11-05T01:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T03:00:00+00:00', + 'price': 61.69, + 'start': '2024-11-05T02:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T04:00:00+00:00', + 'price': 64.6, + 'start': '2024-11-05T03:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T05:00:00+00:00', + 'price': 453.27, + 'start': '2024-11-05T04:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T06:00:00+00:00', + 'price': 996.28, + 'start': '2024-11-05T05:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T07:00:00+00:00', + 'price': 1406.14, + 'start': '2024-11-05T06:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T08:00:00+00:00', + 'price': 1346.54, + 'start': '2024-11-05T07:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T09:00:00+00:00', + 'price': 1150.28, + 'start': '2024-11-05T08:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T10:00:00+00:00', + 'price': 1031.32, + 'start': '2024-11-05T09:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T11:00:00+00:00', + 'price': 927.37, + 'start': '2024-11-05T10:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T12:00:00+00:00', + 'price': 925.05, + 'start': '2024-11-05T11:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T13:00:00+00:00', + 'price': 949.49, + 'start': '2024-11-05T12:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T14:00:00+00:00', + 'price': 1042.03, + 'start': '2024-11-05T13:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T15:00:00+00:00', + 'price': 1258.89, + 'start': '2024-11-05T14:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T16:00:00+00:00', + 'price': 1816.45, + 'start': '2024-11-05T15:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T17:00:00+00:00', + 'price': 2512.65, + 'start': '2024-11-05T16:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T18:00:00+00:00', + 'price': 1819.83, + 'start': '2024-11-05T17:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T19:00:00+00:00', + 'price': 1011.77, + 'start': '2024-11-05T18:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T20:00:00+00:00', + 'price': 835.53, + 'start': '2024-11-05T19:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T21:00:00+00:00', + 'price': 796.19, + 'start': '2024-11-05T20:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T22:00:00+00:00', + 'price': 522.3, + 'start': '2024-11-05T21:00:00+00:00', + }), + dict({ + 'end': '2024-11-05T23:00:00+00:00', + 'price': 289.14, + 'start': '2024-11-05T22:00:00+00:00', + }), + ]), + }) +# --- diff --git a/tests/components/nordpool/test_config_flow.py b/tests/components/nordpool/test_config_flow.py index cfdfc63aca7..1f0e99b65ff 100644 --- a/tests/components/nordpool/test_config_flow.py +++ b/tests/components/nordpool/test_config_flow.py @@ -2,10 +2,11 @@ from __future__ import annotations +from typing import Any from unittest.mock import patch from pynordpool import ( - DeliveryPeriodData, + NordPoolClient, NordPoolConnectionError, NordPoolEmptyResponseError, NordPoolError, @@ -22,10 +23,11 @@ from homeassistant.data_entry_flow import FlowResultType from . import ENTRY_CONFIG from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") -async def test_form(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None: +async def test_form(hass: HomeAssistant, get_client: NordPoolClient) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -34,17 +36,11 @@ async def test_form(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None: assert result["step_id"] == "user" assert result["type"] is FlowResultType.FORM - with ( - patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - ENTRY_CONFIG, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ENTRY_CONFIG, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["version"] == 1 @@ -54,7 +50,7 @@ async def test_form(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None: @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") async def test_single_config_entry( - hass: HomeAssistant, load_int: None, get_data: DeliveryPeriodData + hass: HomeAssistant, load_int: None, get_client: NordPoolClient ) -> None: """Test abort for single config entry.""" @@ -77,7 +73,7 @@ async def test_single_config_entry( ) async def test_cannot_connect( hass: HomeAssistant, - get_data: DeliveryPeriodData, + get_client: NordPoolClient, error_message: Exception, p_error: str, ) -> None: @@ -101,14 +97,10 @@ async def test_cannot_connect( assert result["errors"] == {"base": p_error} - with patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=ENTRY_CONFIG, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=ENTRY_CONFIG, + ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Nord Pool" @@ -119,25 +111,18 @@ async def test_cannot_connect( async def test_reconfigure( hass: HomeAssistant, load_int: MockConfigEntry, - get_data: DeliveryPeriodData, ) -> None: """Test reconfiguration.""" result = await load_int.start_reconfigure_flow(hass) - with ( - patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_AREAS: ["SE3"], - CONF_CURRENCY: "EUR", - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_AREAS: ["SE3"], + CONF_CURRENCY: "EUR", + }, + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -162,7 +147,8 @@ async def test_reconfigure( async def test_reconfigure_cannot_connect( hass: HomeAssistant, load_int: MockConfigEntry, - get_data: DeliveryPeriodData, + aioclient_mock: AiohttpClientMocker, + load_json: list[dict[str, Any]], error_message: Exception, p_error: str, ) -> None: @@ -184,17 +170,13 @@ async def test_reconfigure_cannot_connect( assert result["errors"] == {"base": p_error} - with patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_AREAS: ["SE3"], - CONF_CURRENCY: "EUR", - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_AREAS: ["SE3"], + CONF_CURRENCY: "EUR", + }, + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" diff --git a/tests/components/nordpool/test_coordinator.py b/tests/components/nordpool/test_coordinator.py index 68534237dee..71c4644ea95 100644 --- a/tests/components/nordpool/test_coordinator.py +++ b/tests/components/nordpool/test_coordinator.py @@ -7,8 +7,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from pynordpool import ( - DeliveryPeriodData, NordPoolAuthenticationError, + NordPoolClient, NordPoolEmptyResponseError, NordPoolError, NordPoolResponseError, @@ -28,7 +28,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") async def test_coordinator( hass: HomeAssistant, - get_data: DeliveryPeriodData, + get_client: NordPoolClient, freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: @@ -41,66 +41,72 @@ async def test_coordinator( config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "0.92737" + with ( patch( "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + side_effect=NordPoolError("error"), ) as mock_data, ): - mock_data.return_value = get_data - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "0.92737" - mock_data.reset_mock() - - mock_data.side_effect = NordPoolError("error") freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - assert mock_data.call_count == 4 + assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") assert state.state == STATE_UNAVAILABLE - mock_data.reset_mock() + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + side_effect=NordPoolAuthenticationError("Authentication error"), + ) as mock_data, + ): assert "Authentication error" not in caplog.text - mock_data.side_effect = NordPoolAuthenticationError("Authentication error") freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - assert mock_data.call_count == 4 + assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") assert state.state == STATE_UNAVAILABLE assert "Authentication error" in caplog.text - mock_data.reset_mock() + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + side_effect=NordPoolEmptyResponseError("Empty response"), + ) as mock_data, + ): assert "Empty response" not in caplog.text - mock_data.side_effect = NordPoolEmptyResponseError("Empty response") freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - assert mock_data.call_count == 4 + # Empty responses does not raise + assert mock_data.call_count == 3 state = hass.states.get("sensor.nord_pool_se3_current_price") assert state.state == STATE_UNAVAILABLE assert "Empty response" in caplog.text - mock_data.reset_mock() + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + side_effect=NordPoolResponseError("Response error"), + ) as mock_data, + ): assert "Response error" not in caplog.text - mock_data.side_effect = NordPoolResponseError("Response error") freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - assert mock_data.call_count == 4 + assert mock_data.call_count == 1 state = hass.states.get("sensor.nord_pool_se3_current_price") assert state.state == STATE_UNAVAILABLE assert "Response error" in caplog.text - mock_data.reset_mock() - mock_data.return_value = get_data - mock_data.side_effect = None - freezer.tick(timedelta(hours=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("sensor.nord_pool_se3_current_price") - assert state.state == "1.81645" + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("sensor.nord_pool_se3_current_price") + assert state.state == "1.81645" diff --git a/tests/components/nordpool/test_diagnostics.py b/tests/components/nordpool/test_diagnostics.py index 4639186ecf1..a9dfdd5eca5 100644 --- a/tests/components/nordpool/test_diagnostics.py +++ b/tests/components/nordpool/test_diagnostics.py @@ -2,19 +2,21 @@ from __future__ import annotations +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - load_int: ConfigEntry, + load_int: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test generating diagnostics for a config entry.""" diff --git a/tests/components/nordpool/test_init.py b/tests/components/nordpool/test_init.py index ebebb8b60c1..c9b6167ff3c 100644 --- a/tests/components/nordpool/test_init.py +++ b/tests/components/nordpool/test_init.py @@ -2,10 +2,12 @@ from __future__ import annotations +import json from unittest.mock import patch from pynordpool import ( - DeliveryPeriodData, + API, + NordPoolClient, NordPoolConnectionError, NordPoolEmptyResponseError, NordPoolError, @@ -13,16 +15,21 @@ from pynordpool import ( ) import pytest -from homeassistant.components.nordpool.const import DOMAIN +from homeassistant.components.nordpool.const import CONF_AREAS, DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_CURRENCY from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr, entity_registry as er from . import ENTRY_CONFIG -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_unload_entry(hass: HomeAssistant, get_data: DeliveryPeriodData) -> None: +@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +async def test_unload_entry(hass: HomeAssistant, get_client: NordPoolClient) -> None: """Test load and unload an entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -31,13 +38,7 @@ async def test_unload_entry(hass: HomeAssistant, get_data: DeliveryPeriodData) - ) entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", - return_value=get_data, - ), - ): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED @@ -56,7 +57,7 @@ async def test_unload_entry(hass: HomeAssistant, get_data: DeliveryPeriodData) - ], ) async def test_initial_startup_fails( - hass: HomeAssistant, get_data: DeliveryPeriodData, error: Exception + hass: HomeAssistant, get_client: NordPoolClient, error: Exception ) -> None: """Test load and unload an entry.""" entry = MockConfigEntry( @@ -76,3 +77,100 @@ async def test_initial_startup_fails( await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.freeze_time("2024-11-05T10:00:00+00:00") +async def test_reconfigure_cleans_up_device( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + get_client: NordPoolClient, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test clean up devices due to reconfiguration.""" + nl_json_file = load_fixture("delivery_period_nl.json", DOMAIN) + load_nl_json = json.loads(nl_json_file) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert entry.state is ConfigEntryState.LOADED + + assert device_registry.async_get_device(identifiers={(DOMAIN, "SE3")}) + assert device_registry.async_get_device(identifiers={(DOMAIN, "SE4")}) + assert entity_registry.async_get("sensor.nord_pool_se3_current_price") + assert entity_registry.async_get("sensor.nord_pool_se4_current_price") + assert hass.states.get("sensor.nord_pool_se3_current_price") + assert hass.states.get("sensor.nord_pool_se4_current_price") + + aioclient_mock.clear_requests() + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-04", + "market": "DayAhead", + "deliveryArea": "NL", + "currency": "EUR", + }, + json=load_nl_json, + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-05", + "market": "DayAhead", + "deliveryArea": "NL", + "currency": "EUR", + }, + json=load_nl_json, + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-06", + "market": "DayAhead", + "deliveryArea": "NL", + "currency": "EUR", + }, + json=load_nl_json, + ) + + result = await entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_AREAS: ["NL"], + CONF_CURRENCY: "EUR", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + "areas": [ + "NL", + ], + "currency": "EUR", + } + await hass.async_block_till_done(wait_background_tasks=True) + + assert device_registry.async_get_device(identifiers={(DOMAIN, "NL")}) + assert entity_registry.async_get("sensor.nord_pool_nl_current_price") + assert hass.states.get("sensor.nord_pool_nl_current_price") + + assert not device_registry.async_get_device(identifiers={(DOMAIN, "SE3")}) + assert not entity_registry.async_get("sensor.nord_pool_se3_current_price") + assert not hass.states.get("sensor.nord_pool_se3_current_price") + assert not device_registry.async_get_device(identifiers={(DOMAIN, "SE4")}) + assert not entity_registry.async_get("sensor.nord_pool_se4_current_price") + assert not hass.states.get("sensor.nord_pool_se4_current_price") diff --git a/tests/components/nordpool/test_sensor.py b/tests/components/nordpool/test_sensor.py index 5c2d138cb34..60be1ee3258 100644 --- a/tests/components/nordpool/test_sensor.py +++ b/tests/components/nordpool/test_sensor.py @@ -2,6 +2,12 @@ from __future__ import annotations +from datetime import timedelta +from http import HTTPStatus +from typing import Any + +from freezegun.api import FrozenDateTimeFactory +from pynordpool import API import pytest from syrupy.assertion import SnapshotAssertion @@ -10,7 +16,8 @@ from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from tests.common import snapshot_platform +from tests.common import async_fire_time_changed, snapshot_platform +from tests.test_util.aiohttp import AiohttpClientMocker @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") @@ -38,12 +45,12 @@ async def test_sensor_no_next_price(hass: HomeAssistant, load_int: ConfigEntry) assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.28914" - assert last_price.state == "0.28914" - assert next_price.state == STATE_UNKNOWN + assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z + assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z + assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z" -@pytest.mark.freeze_time("2024-11-05T00:00:00+01:00") +@pytest.mark.freeze_time("2024-11-06T00:00:00+01:00") @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_no_previous_price( hass: HomeAssistant, load_int: ConfigEntry @@ -57,6 +64,135 @@ async def test_sensor_no_previous_price( assert current_price is not None assert last_price is not None assert next_price is not None - assert current_price.state == "0.25073" - assert last_price.state == STATE_UNKNOWN - assert next_price.state == "0.07636" + assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z + assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z + assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z + + +@pytest.mark.freeze_time("2024-11-05T11:00:01+01:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_empty_response( + hass: HomeAssistant, + load_int: ConfigEntry, + load_json: list[dict[str, Any]], + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Nord Pool sensor with empty response.""" + + responses = list(load_json) + + current_price = hass.states.get("sensor.nord_pool_se3_current_price") + last_price = hass.states.get("sensor.nord_pool_se3_previous_price") + next_price = hass.states.get("sensor.nord_pool_se3_next_price") + assert current_price is not None + assert last_price is not None + assert next_price is not None + assert current_price.state == "0.92737" + assert last_price.state == "1.03132" + assert next_price.state == "0.92505" + + aioclient_mock.clear_requests() + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-04", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[1], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-05", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[0], + ) + # Future date without data should return 204 + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-06", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + status=HTTPStatus.NO_CONTENT, + ) + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # All prices should be known as tomorrow is not loaded by sensors + + current_price = hass.states.get("sensor.nord_pool_se3_current_price") + last_price = hass.states.get("sensor.nord_pool_se3_previous_price") + next_price = hass.states.get("sensor.nord_pool_se3_next_price") + assert current_price is not None + assert last_price is not None + assert next_price is not None + assert current_price.state == "0.92505" + assert last_price.state == "0.92737" + assert next_price.state == "0.94949" + + aioclient_mock.clear_requests() + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-04", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[1], + ) + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-05", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + json=responses[0], + ) + # Future date without data should return 204 + aioclient_mock.request( + "GET", + url=API + "/DayAheadPrices", + params={ + "date": "2024-11-06", + "market": "DayAhead", + "deliveryArea": "SE3,SE4", + "currency": "SEK", + }, + status=HTTPStatus.NO_CONTENT, + ) + + freezer.move_to("2024-11-05T22:00:01+00:00") + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # Current and last price should be known, next price should be unknown + # as api responds with empty data (204) + + current_price = hass.states.get("sensor.nord_pool_se3_current_price") + last_price = hass.states.get("sensor.nord_pool_se3_previous_price") + next_price = hass.states.get("sensor.nord_pool_se3_next_price") + assert current_price is not None + assert last_price is not None + assert next_price is not None + assert current_price.state == "0.28914" + assert last_price.state == "0.5223" + assert next_price.state == STATE_UNKNOWN diff --git a/tests/components/nordpool/test_services.py b/tests/components/nordpool/test_services.py new file mode 100644 index 00000000000..6d6af685d28 --- /dev/null +++ b/tests/components/nordpool/test_services.py @@ -0,0 +1,139 @@ +"""Test services in Nord Pool.""" + +from unittest.mock import patch + +from pynordpool import ( + NordPoolAuthenticationError, + NordPoolEmptyResponseError, + NordPoolError, +) +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.nordpool.const import DOMAIN +from homeassistant.components.nordpool.services import ( + ATTR_AREAS, + ATTR_CONFIG_ENTRY, + ATTR_CURRENCY, + SERVICE_GET_PRICES_FOR_DATE, +) +from homeassistant.const import ATTR_DATE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from tests.common import MockConfigEntry + +TEST_SERVICE_DATA = { + ATTR_CONFIG_ENTRY: "to_replace", + ATTR_DATE: "2024-11-05", + ATTR_AREAS: "SE3", + ATTR_CURRENCY: "EUR", +} +TEST_SERVICE_DATA_USE_DEFAULTS = { + ATTR_CONFIG_ENTRY: "to_replace", + ATTR_DATE: "2024-11-05", +} + + +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +async def test_service_call( + hass: HomeAssistant, + load_int: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test get_prices_for_date service call.""" + + service_data = TEST_SERVICE_DATA.copy() + service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PRICES_FOR_DATE, + service_data, + blocking=True, + return_response=True, + ) + + assert response == snapshot + price_value = response["SE3"][0]["price"] + + service_data = TEST_SERVICE_DATA_USE_DEFAULTS.copy() + service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PRICES_FOR_DATE, + service_data, + blocking=True, + return_response=True, + ) + + assert "SE3" in response + assert response["SE3"][0]["price"] == price_value + + +@pytest.mark.parametrize( + ("error", "key"), + [ + (NordPoolAuthenticationError, "authentication_error"), + (NordPoolEmptyResponseError, "empty_response"), + (NordPoolError, "connection_error"), + ], +) +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +async def test_service_call_failures( + hass: HomeAssistant, + load_int: MockConfigEntry, + error: Exception, + key: str, +) -> None: + """Test get_prices_for_date service call when it fails.""" + service_data = TEST_SERVICE_DATA.copy() + service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id + + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + side_effect=error, + ), + pytest.raises(ServiceValidationError) as err, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PRICES_FOR_DATE, + service_data, + blocking=True, + return_response=True, + ) + assert err.value.translation_key == key + + +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +async def test_service_call_config_entry_bad_state( + hass: HomeAssistant, + load_int: MockConfigEntry, +) -> None: + """Test get_prices_for_date service call when config entry bad state.""" + + with pytest.raises(ServiceValidationError) as err: + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PRICES_FOR_DATE, + TEST_SERVICE_DATA, + blocking=True, + return_response=True, + ) + assert err.value.translation_key == "entry_not_found" + + service_data = TEST_SERVICE_DATA.copy() + service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id + await hass.config_entries.async_unload(load_int.entry_id) + await hass.async_block_till_done() + + with pytest.raises(ServiceValidationError) as err: + await hass.services.async_call( + DOMAIN, + SERVICE_GET_PRICES_FOR_DATE, + service_data, + blocking=True, + return_response=True, + ) + assert err.value.translation_key == "entry_not_loaded" diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index ad987325b97..96d5e815ff0 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -37,7 +37,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index bc00df126e5..5e3ba384b2d 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -6,7 +6,7 @@ from unittest.mock import patch from homeassistant.components.nuheat.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .mocks import ( MOCK_CONFIG_ENTRY, diff --git a/tests/components/nuki/snapshots/test_binary_sensor.ambr b/tests/components/nuki/snapshots/test_binary_sensor.ambr index 55976bcb433..e48cc55bfb3 100644 --- a/tests/components/nuki/snapshots/test_binary_sensor.ambr +++ b/tests/components/nuki/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -146,6 +149,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -193,6 +197,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nuki/snapshots/test_lock.ambr b/tests/components/nuki/snapshots/test_lock.ambr index 24c80e7b487..2d80110a5cc 100644 --- a/tests/components/nuki/snapshots/test_lock.ambr +++ b/tests/components/nuki/snapshots/test_lock.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nuki/snapshots/test_sensor.ambr b/tests/components/nuki/snapshots/test_sensor.ambr index a319104fbc3..5be025727be 100644 --- a/tests/components/nuki/snapshots/test_sensor.ambr +++ b/tests/components/nuki/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index d4ddc261f1e..efc6c6c5abc 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -6,11 +6,11 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.nuki.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .mock import DHCP_FORMATTED_MAC, HOST, MOCK_INFO, NAME, setup_nuki_integration @@ -151,9 +151,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: """Test that DHCP discovery for new bridge works.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( - hostname=NAME, ip=HOST, macaddress=DHCP_FORMATTED_MAC - ), + data=DhcpServiceInfo(hostname=NAME, ip=HOST, macaddress=DHCP_FORMATTED_MAC), context={"source": config_entries.SOURCE_DHCP}, ) @@ -196,9 +194,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: await setup_nuki_integration(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( - hostname=NAME, ip=HOST, macaddress=DHCP_FORMATTED_MAC - ), + data=DhcpServiceInfo(hostname=NAME, ip=HOST, macaddress=DHCP_FORMATTED_MAC), context={"source": config_entries.SOURCE_DHCP}, ) diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index ffebd62fcbf..6b24c15f18a 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -243,18 +243,26 @@ async def test_action_legacy( async def test_capabilities( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test getting capabilities.""" - entry = entity_registry.async_get_or_create( - DOMAIN, "test", "5678", device_id="abcdefgh" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + DOMAIN, "test", "5678", device_id=device_entry.id ) capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": entry.id, + "entity_id": entity_entry.id, "type": "set_value", }, ) @@ -267,18 +275,26 @@ async def test_capabilities( async def test_capabilities_legacy( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test getting capabilities.""" - entry = entity_registry.async_get_or_create( - DOMAIN, "test", "5678", device_id="abcdefgh" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + DOMAIN, "test", "5678", device_id=device_entry.id ) capabilities = await device_action.async_get_action_capabilities( hass, { "domain": DOMAIN, "device_id": "abcdefgh", - "entity_id": entry.entity_id, + "entity_id": entity_entry.entity_id, "type": "set_value", }, ) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 31d99dc55d7..7b19879d873 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -38,7 +38,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @@ -974,7 +974,7 @@ async def test_name(hass: HomeAssistant) -> None: async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test number platform via config entry.""" async_add_entities([entity1, entity2, entity3, entity4]) diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 537b6aba5ac..ed9c87f2f90 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import patch from aionut import NUTError, NUTLoginError from homeassistant import config_entries, setup -from homeassistant.components import zeroconf from homeassistant.components.nut.const import DOMAIN from homeassistant.const import ( CONF_ALIAS, @@ -20,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .util import _get_mock_nutclient @@ -38,7 +38,7 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.5"), ip_addresses=[ip_address("192.168.1.5")], hostname="mock_hostname", diff --git a/tests/components/nyt_games/snapshots/test_init.ambr b/tests/components/nyt_games/snapshots/test_init.ambr index 383bed0e106..d9ce6f15a4d 100644 --- a/tests/components/nyt_games/snapshots/test_init.ambr +++ b/tests/components/nyt_games/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -67,6 +69,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/nyt_games/snapshots/test_sensor.ambr b/tests/components/nyt_games/snapshots/test_sensor.ambr index 84b74a26f0d..8201c26739c 100644 --- a/tests/components/nyt_games/snapshots/test_sensor.ambr +++ b/tests/components/nyt_games/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -108,6 +110,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -157,6 +160,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -207,6 +211,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -257,6 +262,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -307,6 +313,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -357,6 +364,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -407,6 +415,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -458,6 +467,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -509,6 +519,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -559,6 +570,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/obihai/__init__.py b/tests/components/obihai/__init__.py index b88f0a5c874..7b483514dcf 100644 --- a/tests/components/obihai/__init__.py +++ b/tests/components/obihai/__init__.py @@ -1,7 +1,7 @@ """Tests for the Obihai Integration.""" -from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo USER_INPUT = { CONF_HOST: "10.10.10.30", @@ -9,7 +9,7 @@ USER_INPUT = { CONF_USERNAME: "admin", } -DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( +DHCP_SERVICE_INFO = DhcpServiceInfo( hostname="obi200", ip="192.168.1.100", macaddress="9cadef000000", diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index e0696486718..d7d7e43e99c 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -6,10 +6,11 @@ from unittest.mock import patch from pyoctoprintapi import ApiError, DiscoverySettings from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf from homeassistant.components.octoprint.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -183,7 +184,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -252,7 +253,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={ @@ -525,7 +526,7 @@ async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -551,7 +552,7 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={ diff --git a/tests/components/ohme/conftest.py b/tests/components/ohme/conftest.py index 90395feeb6b..3d3db730d08 100644 --- a/tests/components/ohme/conftest.py +++ b/tests/components/ohme/conftest.py @@ -53,6 +53,10 @@ def mock_client(): client.async_login.return_value = True client.status = ChargerStatus.CHARGING client.power = ChargerPower(0, 0, 0, 0) + + client.target_soc = 50 + client.target_time = (8, 0) + client.battery = 80 client.serial = "chargerid" client.ct_connected = True client.energy = 1000 diff --git a/tests/components/ohme/snapshots/test_button.ambr b/tests/components/ohme/snapshots/test_button.ambr index 32de16208f4..b276e8c3c42 100644 --- a/tests/components/ohme/snapshots/test_button.ambr +++ b/tests/components/ohme/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ohme/snapshots/test_init.ambr b/tests/components/ohme/snapshots/test_init.ambr index e3ed339b78a..ccf09f546cf 100644 --- a/tests/components/ohme/snapshots/test_init.ambr +++ b/tests/components/ohme/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/ohme/snapshots/test_number.ambr b/tests/components/ohme/snapshots/test_number.ambr new file mode 100644 index 00000000000..dbcf6134252 --- /dev/null +++ b/tests/components/ohme/snapshots/test_number.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_numbers[number.ohme_home_pro_target_percentage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.ohme_home_pro_target_percentage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target percentage', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'target_percentage', + 'unique_id': 'chargerid_target_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.ohme_home_pro_target_percentage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Target percentage', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.ohme_home_pro_target_percentage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- diff --git a/tests/components/ohme/snapshots/test_select.ambr b/tests/components/ohme/snapshots/test_select.ambr new file mode 100644 index 00000000000..8eec0556889 --- /dev/null +++ b/tests/components/ohme/snapshots/test_select.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_selects[select.ohme_home_pro_charge_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'smart_charge', + 'max_charge', + 'paused', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.ohme_home_pro_charge_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge mode', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_mode', + 'unique_id': 'chargerid_charge_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.ohme_home_pro_charge_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Charge mode', + 'options': list([ + 'smart_charge', + 'max_charge', + 'paused', + ]), + }), + 'context': , + 'entity_id': 'select.ohme_home_pro_charge_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/ohme/snapshots/test_sensor.ambr b/tests/components/ohme/snapshots/test_sensor.ambr index fbffa5b7e5d..fc28b3b011c 100644 --- a/tests/components/ohme/snapshots/test_sensor.ambr +++ b/tests/components/ohme/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -104,6 +106,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -159,6 +162,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -217,9 +221,12 @@ 'pending_approval', 'charging', 'plugged_in', + 'paused', + 'finished', ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -256,6 +263,8 @@ 'pending_approval', 'charging', 'plugged_in', + 'paused', + 'finished', ]), }), 'context': , @@ -266,3 +275,55 @@ 'state': 'charging', }) # --- +# name: test_sensors[sensor.ohme_home_pro_vehicle_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ohme_home_pro_vehicle_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Vehicle battery', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_battery', + 'unique_id': 'chargerid_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.ohme_home_pro_vehicle_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Ohme Home Pro Vehicle battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ohme_home_pro_vehicle_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- diff --git a/tests/components/ohme/snapshots/test_services.ambr b/tests/components/ohme/snapshots/test_services.ambr new file mode 100644 index 00000000000..91917ed6925 --- /dev/null +++ b/tests/components/ohme/snapshots/test_services.ambr @@ -0,0 +1,12 @@ +# serializer version: 1 +# name: test_list_charge_slots + dict({ + 'slots': list([ + dict({ + 'end': '2024-12-30T04:30:39+00:00', + 'energy': 2.042, + 'start': '2024-12-30T04:00:00+00:00', + }), + ]), + }) +# --- diff --git a/tests/components/ohme/snapshots/test_switch.ambr b/tests/components/ohme/snapshots/test_switch.ambr new file mode 100644 index 00000000000..49bf5d5709a --- /dev/null +++ b/tests/components/ohme/snapshots/test_switch.ambr @@ -0,0 +1,142 @@ +# serializer version: 1 +# name: test_switches[switch.ohme_home_pro_lock_buttons-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ohme_home_pro_lock_buttons', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock buttons', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lock_buttons', + 'unique_id': 'chargerid_lock_buttons', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ohme_home_pro_lock_buttons-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Lock buttons', + }), + 'context': , + 'entity_id': 'switch.ohme_home_pro_lock_buttons', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ohme_home_pro_require_approval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ohme_home_pro_require_approval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Require approval', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'require_approval', + 'unique_id': 'chargerid_require_approval', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ohme_home_pro_require_approval-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Require approval', + }), + 'context': , + 'entity_id': 'switch.ohme_home_pro_require_approval', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ohme_home_pro_sleep_when_inactive-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ohme_home_pro_sleep_when_inactive', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sleep when inactive', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sleep_when_inactive', + 'unique_id': 'chargerid_sleep_when_inactive', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ohme_home_pro_sleep_when_inactive-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Sleep when inactive', + }), + 'context': , + 'entity_id': 'switch.ohme_home_pro_sleep_when_inactive', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/ohme/snapshots/test_time.ambr b/tests/components/ohme/snapshots/test_time.ambr new file mode 100644 index 00000000000..8c85fc2298e --- /dev/null +++ b/tests/components/ohme/snapshots/test_time.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_time[time.ohme_home_pro_target_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'time', + 'entity_category': None, + 'entity_id': 'time.ohme_home_pro_target_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target time', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'target_time', + 'unique_id': 'chargerid_target_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_time[time.ohme_home_pro_target_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Target time', + }), + 'context': , + 'entity_id': 'time.ohme_home_pro_target_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '08:00:00', + }) +# --- diff --git a/tests/components/ohme/test_number.py b/tests/components/ohme/test_number.py new file mode 100644 index 00000000000..9cfce2a850f --- /dev/null +++ b/tests/components/ohme/test_number.py @@ -0,0 +1,55 @@ +"""Tests for numbers.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_numbers( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the Ohme sensors.""" + with patch("homeassistant.components.ohme.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_number( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the number set.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_VALUE: 100, + }, + target={ + ATTR_ENTITY_ID: "number.ohme_home_pro_target_percentage", + }, + blocking=True, + ) + + assert len(mock_client.async_set_target.mock_calls) == 1 diff --git a/tests/components/ohme/test_select.py b/tests/components/ohme/test_select.py new file mode 100644 index 00000000000..5aeebc1f477 --- /dev/null +++ b/tests/components/ohme/test_select.py @@ -0,0 +1,72 @@ +"""Tests for selects.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from ohme import ChargerMode +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_selects( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the Ohme selects.""" + with patch("homeassistant.components.ohme.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_select_option( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test selecting an option in the Ohme select entity.""" + mock_client.mode = ChargerMode.SMART_CHARGE + mock_client.async_set_mode = AsyncMock() + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.ohme_home_pro_charge_mode") + assert state is not None + assert state.state == "smart_charge" + + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": "select.ohme_home_pro_charge_mode", + "option": "max_charge", + }, + blocking=True, + ) + + mock_client.async_set_mode.assert_called_once_with("max_charge") + assert state.state == "smart_charge" + + +async def test_select_unavailable( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test that the select entity shows as unavailable when no mode is set.""" + mock_client.mode = None + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.ohme_home_pro_charge_mode") + assert state is not None + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/ohme/test_services.py b/tests/components/ohme/test_services.py new file mode 100644 index 00000000000..76c7ce94b57 --- /dev/null +++ b/tests/components/ohme/test_services.py @@ -0,0 +1,70 @@ +"""Tests for services.""" + +from unittest.mock import MagicMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.ohme.const import DOMAIN +from homeassistant.components.ohme.services import ( + ATTR_CONFIG_ENTRY, + SERVICE_LIST_CHARGE_SLOTS, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_list_charge_slots( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test list charge slots service.""" + + await setup_integration(hass, mock_config_entry) + + mock_client.slots = [ + { + "start": "2024-12-30T04:00:00+00:00", + "end": "2024-12-30T04:30:39+00:00", + "energy": 2.042, + } + ] + + assert snapshot == await hass.services.async_call( + DOMAIN, + "list_charge_slots", + { + ATTR_CONFIG_ENTRY: mock_config_entry.entry_id, + }, + blocking=True, + return_response=True, + ) + + +async def test_list_charge_slots_exception( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test list charge slots service.""" + + await setup_integration(hass, mock_config_entry) + + # Test error + with pytest.raises( + ServiceValidationError, match="Invalid config entry provided. Got invalid" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_LIST_CHARGE_SLOTS, + {ATTR_CONFIG_ENTRY: "invalid"}, + blocking=True, + return_response=True, + ) diff --git a/tests/components/ohme/test_switch.py b/tests/components/ohme/test_switch.py new file mode 100644 index 00000000000..b16b70d67f8 --- /dev/null +++ b/tests/components/ohme/test_switch.py @@ -0,0 +1,72 @@ +"""Tests for switches.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_switches( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the Ohme switches.""" + with patch("homeassistant.components.ohme.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_switch_on( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the switch turn_on action.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "switch.ohme_home_pro_lock_buttons", + }, + blocking=True, + ) + + assert len(mock_client.async_set_configuration_value.mock_calls) == 1 + + +async def test_switch_off( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the switch turn_off action.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "switch.ohme_home_pro_lock_buttons", + }, + blocking=True, + ) + + assert len(mock_client.async_set_configuration_value.mock_calls) == 1 diff --git a/tests/components/ohme/test_time.py b/tests/components/ohme/test_time.py new file mode 100644 index 00000000000..0562dfa124c --- /dev/null +++ b/tests/components/ohme/test_time.py @@ -0,0 +1,55 @@ +"""Tests for time.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.time import ( + ATTR_TIME, + DOMAIN as TIME_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_time( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the Ohme sensors.""" + with patch("homeassistant.components.ohme.PLATFORMS", [Platform.TIME]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_time( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the time set.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + TIME_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_TIME: "00:00:00", + }, + target={ + ATTR_ENTITY_ID: "time.ohme_home_pro_target_time", + }, + blocking=True, + ) + + assert len(mock_client.async_set_target.mock_calls) == 1 diff --git a/tests/components/ollama/snapshots/test_conversation.ambr b/tests/components/ollama/snapshots/test_conversation.ambr index e4dd7cd00bb..93f3b03d9af 100644 --- a/tests/components/ollama/snapshots/test_conversation.ambr +++ b/tests/components/ollama/snapshots/test_conversation.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_unknown_hass_api dict({ - 'conversation_id': None, + 'conversation_id': '1234', 'response': IntentResponse( card=dict({ }), @@ -20,7 +20,7 @@ speech=dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Error preparing LLM API: API non-existing not found', + 'speech': 'Error preparing LLM API', }), }), speech_slots=dict({ diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index 66dc8a0c603..db641ba703b 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -1,5 +1,6 @@ """Tests for the Ollama integration.""" +from collections.abc import AsyncGenerator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -18,6 +19,21 @@ from homeassistant.helpers import intent, llm from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_ulid_tools(): + """Mock generated ULIDs for tool calls.""" + with patch("homeassistant.helpers.llm.ulid_now", return_value="mock-tool-call"): + yield + + +async def stream_generator(response: dict | list[dict]) -> AsyncGenerator[dict]: + """Generate a response from the assistant.""" + if not isinstance(response, list): + response = [response] + for msg in response: + yield msg + + @pytest.mark.parametrize("agent_id", [None, "conversation.mock_title"]) async def test_chat( hass: HomeAssistant, @@ -35,7 +51,9 @@ async def test_chat( with patch( "ollama.AsyncClient.chat", - return_value={"message": {"role": "assistant", "content": "test response"}}, + return_value=stream_generator( + {"message": {"role": "assistant", "content": "test response"}} + ), ) as mock_chat: result = await conversation.async_converse( hass, @@ -51,13 +69,13 @@ async def test_chat( assert args["model"] == "test model" assert args["messages"] == [ - Message({"role": "system", "content": prompt}), - Message({"role": "user", "content": "test message"}), + Message(role="system", content=prompt), + Message(role="user", content="test message"), ] - assert ( - result.response.response_type == intent.IntentResponseType.ACTION_DONE - ), result + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE, ( + result + ) assert result.response.speech["plain"]["speech"] == "test response" # Test Conversation tracing @@ -74,6 +92,53 @@ async def test_chat( assert "Current time is" in detail_event["data"]["messages"][0]["content"] +async def test_chat_stream( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test chat messages are assembled across streamed responses.""" + + entry = MockConfigEntry() + entry.add_to_hass(hass) + + with patch( + "ollama.AsyncClient.chat", + return_value=stream_generator( + [ + {"message": {"role": "assistant", "content": "test "}}, + { + "message": {"role": "assistant", "content": "response"}, + "done": True, + "done_reason": "stop", + }, + ], + ), + ) as mock_chat: + result = await conversation.async_converse( + hass, + "test message", + None, + Context(), + agent_id=mock_config_entry.entry_id, + ) + + assert mock_chat.call_count == 1 + args = mock_chat.call_args.kwargs + prompt = args["messages"][0]["content"] + + assert args["model"] == "test model" + assert args["messages"] == [ + Message(role="system", content=prompt), + Message(role="user", content="test message"), + ] + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE, ( + result + ) + assert result.response.speech["plain"]["speech"] == "test response" + + async def test_template_variables( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: @@ -96,7 +161,9 @@ async def test_template_variables( patch("ollama.AsyncClient.list"), patch( "ollama.AsyncClient.chat", - return_value={"message": {"role": "assistant", "content": "test response"}}, + return_value=stream_generator( + {"message": {"role": "assistant", "content": "test response"}} + ), ) as mock_chat, patch("homeassistant.auth.AuthManager.async_get_user", return_value=mock_user), ): @@ -106,9 +173,9 @@ async def test_template_variables( hass, "hello", None, context, agent_id=mock_config_entry.entry_id ) - assert ( - result.response.response_type == intent.IntentResponseType.ACTION_DONE - ), result + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE, ( + result + ) args = mock_chat.call_args.kwargs prompt = args["messages"][0]["content"] @@ -163,26 +230,30 @@ async def test_function_call( def completion_result(*args, messages, **kwargs): for message in messages: if message["role"] == "tool": - return { - "message": { - "role": "assistant", - "content": "I have successfully called the function", - } - } - - return { - "message": { - "role": "assistant", - "tool_calls": [ + return stream_generator( { - "function": { - "name": "test_tool", - "arguments": tool_args, + "message": { + "role": "assistant", + "content": "I have successfully called the function", } } - ], + ) + + return stream_generator( + { + "message": { + "role": "assistant", + "tool_calls": [ + { + "function": { + "name": "test_tool", + "arguments": tool_args, + } + } + ], + } } - } + ) with patch( "ollama.AsyncClient.chat", @@ -205,6 +276,7 @@ async def test_function_call( mock_tool.async_call.assert_awaited_once_with( hass, llm.ToolInput( + id="mock-tool-call", tool_name="test_tool", tool_args=expected_tool_args, ), @@ -243,26 +315,30 @@ async def test_function_exception( def completion_result(*args, messages, **kwargs): for message in messages: if message["role"] == "tool": - return { - "message": { - "role": "assistant", - "content": "There was an error calling the function", - } - } - - return { - "message": { - "role": "assistant", - "tool_calls": [ + return stream_generator( { - "function": { - "name": "test_tool", - "arguments": {"param1": "test_value"}, + "message": { + "role": "assistant", + "content": "There was an error calling the function", } } - ], + ) + + return stream_generator( + { + "message": { + "role": "assistant", + "tool_calls": [ + { + "function": { + "name": "test_tool", + "arguments": {"param1": "test_value"}, + } + } + ], + } } - } + ) with patch( "ollama.AsyncClient.chat", @@ -285,6 +361,7 @@ async def test_function_exception( mock_tool.async_call.assert_awaited_once_with( hass, llm.ToolInput( + id="mock-tool-call", tool_name="test_tool", tool_args={"param1": "test_value"}, ), @@ -316,7 +393,11 @@ async def test_unknown_hass_api( await hass.async_block_till_done() result = await conversation.async_converse( - hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id + hass, + "hello", + "1234", + Context(), + agent_id=mock_config_entry.entry_id, ) assert result == snapshot @@ -331,7 +412,9 @@ async def test_message_history_trimming( def response(*args, **kwargs) -> dict: nonlocal response_idx response_idx += 1 - return {"message": {"role": "assistant", "content": f"response {response_idx}"}} + return stream_generator( + {"message": {"role": "assistant", "content": f"response {response_idx}"}} + ) with patch( "ollama.AsyncClient.chat", @@ -341,7 +424,7 @@ async def test_message_history_trimming( for i in range(5): result = await conversation.async_converse( hass, - f"message {i+1}", + f"message {i + 1}", conversation_id="1234", context=Context(), agent_id=mock_config_entry.entry_id, @@ -419,70 +502,19 @@ async def test_message_history_trimming( assert args[4].kwargs["messages"][5]["content"] == "message 5" -async def test_message_history_pruning( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component -) -> None: - """Test that old message histories are pruned.""" - with patch( - "ollama.AsyncClient.chat", - return_value={"message": {"role": "assistant", "content": "test response"}}, - ): - # Create 3 different message histories - conversation_ids: list[str] = [] - for i in range(3): - result = await conversation.async_converse( - hass, - f"message {i+1}", - conversation_id=None, - context=Context(), - agent_id=mock_config_entry.entry_id, - ) - assert ( - result.response.response_type == intent.IntentResponseType.ACTION_DONE - ), result - assert isinstance(result.conversation_id, str) - conversation_ids.append(result.conversation_id) - - agent = conversation.get_agent_manager(hass).async_get_agent( - mock_config_entry.entry_id - ) - assert len(agent._history) == 3 - assert agent._history.keys() == set(conversation_ids) - - # Modify the timestamps of the first 2 histories so they will be pruned - # on the next cycle. - for conversation_id in conversation_ids[:2]: - # Move back 2 hours - agent._history[conversation_id].timestamp -= 2 * 60 * 60 - - # Next cycle - result = await conversation.async_converse( - hass, - "test message", - conversation_id=None, - context=Context(), - agent_id=mock_config_entry.entry_id, - ) - assert ( - result.response.response_type == intent.IntentResponseType.ACTION_DONE - ), result - - # Only the most recent histories should remain - assert len(agent._history) == 2 - assert conversation_ids[-1] in agent._history - assert result.conversation_id in agent._history - - async def test_message_history_unlimited( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component ) -> None: """Test that message history is not trimmed when max_history = 0.""" conversation_id = "1234" + + def stream(*args, **kwargs) -> AsyncGenerator[dict]: + return stream_generator( + {"message": {"role": "assistant", "content": "test response"}} + ) + with ( - patch( - "ollama.AsyncClient.chat", - return_value={"message": {"role": "assistant", "content": "test response"}}, - ), + patch("ollama.AsyncClient.chat", side_effect=stream) as mock_chat, ): hass.config_entries.async_update_entry( mock_config_entry, options={ollama.CONF_MAX_HISTORY: 0} @@ -490,7 +522,7 @@ async def test_message_history_unlimited( for i in range(100): result = await conversation.async_converse( hass, - f"message {i+1}", + f"message {i + 1}", conversation_id=conversation_id, context=Context(), agent_id=mock_config_entry.entry_id, @@ -499,13 +531,13 @@ async def test_message_history_unlimited( result.response.response_type == intent.IntentResponseType.ACTION_DONE ), result - agent = conversation.get_agent_manager(hass).async_get_agent( - mock_config_entry.entry_id + args = mock_chat.call_args_list + assert len(args) == 100 + recorded_messages = args[-1].kwargs["messages"] + message_count = sum( + (message["role"] == "user") for message in recorded_messages ) - - assert len(agent._history) == 1 - assert conversation_id in agent._history - assert agent._history[conversation_id].num_user_messages == 100 + assert message_count == 100 async def test_error_handling( @@ -599,7 +631,9 @@ async def test_options( """Test that options are passed correctly to ollama client.""" with patch( "ollama.AsyncClient.chat", - return_value={"message": {"role": "assistant", "content": "test response"}}, + return_value=stream_generator( + {"message": {"role": "assistant", "content": "test response"}} + ), ) as mock_chat: await conversation.async_converse( hass, diff --git a/tests/components/omnilogic/snapshots/test_sensor.ambr b/tests/components/omnilogic/snapshots/test_sensor.ambr index a4ea7f02a03..b6eb07dbe26 100644 --- a/tests/components/omnilogic/snapshots/test_sensor.ambr +++ b/tests/components/omnilogic/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/omnilogic/snapshots/test_switch.ambr b/tests/components/omnilogic/snapshots/test_switch.ambr index a5d77f1adcf..cc1a2e226fc 100644 --- a/tests/components/omnilogic/snapshots/test_switch.ambr +++ b/tests/components/omnilogic/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/onboarding/snapshots/test_views.ambr b/tests/components/onboarding/snapshots/test_views.ambr new file mode 100644 index 00000000000..2d084bd9ade --- /dev/null +++ b/tests/components/onboarding/snapshots/test_views.ambr @@ -0,0 +1,68 @@ +# serializer version: 1 +# name: test_onboarding_backup_info + dict({ + 'backups': list([ + dict({ + 'addons': list([ + dict({ + 'name': 'Test', + 'slug': 'test', + 'version': '1.0.0', + }), + ]), + 'agents': dict({ + 'backup.local': dict({ + 'protected': True, + 'size': 0, + }), + }), + 'backup_id': 'abc123', + 'database_included': True, + 'date': '1970-01-01T00:00:00.000Z', + 'extra_metadata': dict({ + 'instance_id': 'abc123', + 'with_automatic_settings': True, + }), + 'failed_agent_ids': list([ + ]), + 'folders': list([ + 'media', + 'share', + ]), + 'homeassistant_included': True, + 'homeassistant_version': '2024.12.0', + 'name': 'Test', + 'with_automatic_settings': True, + }), + dict({ + 'addons': list([ + ]), + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'size': 0, + }), + }), + 'backup_id': 'def456', + 'database_included': False, + 'date': '1980-01-01T00:00:00.000Z', + 'extra_metadata': dict({ + 'instance_id': 'unknown_uuid', + 'with_automatic_settings': True, + }), + 'failed_agent_ids': list([ + ]), + 'folders': list([ + 'media', + 'share', + ]), + 'homeassistant_included': True, + 'homeassistant_version': '2024.12.0', + 'name': 'Test 2', + 'with_automatic_settings': None, + }), + ]), + 'last_non_idle_event': None, + 'state': 'idle', + }) +# --- diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 35f6b7d739c..99623cb6efe 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -3,15 +3,18 @@ import asyncio from collections.abc import AsyncGenerator from http import HTTPStatus +from io import StringIO import os from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch import pytest +from syrupy import SnapshotAssertion -from homeassistant.components import onboarding +from homeassistant.components import backup, onboarding from homeassistant.components.onboarding import const, views from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry as ar from homeassistant.setup import async_setup_component @@ -649,12 +652,28 @@ async def test_onboarding_installation_type( assert resp_content["installation_type"] == "Home Assistant Core" -async def test_onboarding_installation_type_after_done( +@pytest.mark.parametrize( + ("method", "view", "kwargs"), + [ + ("get", "installation_type", {}), + ("get", "backup/info", {}), + ( + "post", + "backup/restore", + {"json": {"backup_id": "abc123", "agent_id": "test"}}, + ), + ("post", "backup/upload", {}), + ], +) +async def test_onboarding_view_after_done( hass: HomeAssistant, hass_storage: dict[str, Any], hass_client: ClientSessionGenerator, + method: str, + view: str, + kwargs: dict[str, Any], ) -> None: - """Test raising for installation type after onboarding.""" + """Test raising after onboarding.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) assert await async_setup_component(hass, "onboarding", {}) @@ -662,7 +681,7 @@ async def test_onboarding_installation_type_after_done( client = await hass_client() - resp = await client.get("/api/onboarding/installation_type") + resp = await client.request(method, f"/api/onboarding/{view}", **kwargs) assert resp.status == 401 @@ -726,3 +745,345 @@ async def test_complete_onboarding( listener_3 = Mock() onboarding.async_add_listener(hass, listener_3) listener_3.assert_called_once_with() + + +@pytest.mark.parametrize( + ("method", "view", "kwargs"), + [ + ("get", "backup/info", {}), + ( + "post", + "backup/restore", + {"json": {"backup_id": "abc123", "agent_id": "test"}}, + ), + ("post", "backup/upload", {}), + ], +) +async def test_onboarding_backup_view_without_backup( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + method: str, + view: str, + kwargs: dict[str, Any], +) -> None: + """Test interacting with backup wievs when backup integration is missing.""" + mock_storage(hass_storage, {"done": []}) + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + client = await hass_client() + + resp = await client.request(method, f"/api/onboarding/{view}", **kwargs) + + assert resp.status == 500 + assert await resp.json() == {"code": "backup_disabled"} + + +async def test_onboarding_backup_info( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test returning installation type during onboarding.""" + mock_storage(hass_storage, {"done": []}) + + assert await async_setup_component(hass, "onboarding", {}) + assert await async_setup_component(hass, "backup", {}) + await hass.async_block_till_done() + + client = await hass_client() + + backups = { + "abc123": backup.ManagerBackup( + addons=[backup.AddonInfo(name="Test", slug="test", version="1.0.0")], + agents={ + "backup.local": backup.manager.AgentBackupStatus(protected=True, size=0) + }, + backup_id="abc123", + date="1970-01-01T00:00:00.000Z", + database_included=True, + extra_metadata={"instance_id": "abc123", "with_automatic_settings": True}, + folders=[backup.Folder.MEDIA, backup.Folder.SHARE], + homeassistant_included=True, + homeassistant_version="2024.12.0", + name="Test", + failed_agent_ids=[], + with_automatic_settings=True, + ), + "def456": backup.ManagerBackup( + addons=[], + agents={ + "test.remote": backup.manager.AgentBackupStatus(protected=True, size=0) + }, + backup_id="def456", + date="1980-01-01T00:00:00.000Z", + database_included=False, + extra_metadata={ + "instance_id": "unknown_uuid", + "with_automatic_settings": True, + }, + folders=[backup.Folder.MEDIA, backup.Folder.SHARE], + homeassistant_included=True, + homeassistant_version="2024.12.0", + name="Test 2", + failed_agent_ids=[], + with_automatic_settings=None, + ), + } + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backups", + return_value=(backups, {}), + ): + resp = await client.get("/api/onboarding/backup/info") + + assert resp.status == 200 + assert await resp.json() == snapshot + + +@pytest.mark.parametrize( + ("params", "expected_kwargs"), + [ + ( + {"backup_id": "abc123", "agent_id": "backup.local"}, + { + "agent_id": "backup.local", + "password": None, + "restore_addons": None, + "restore_database": True, + "restore_folders": None, + "restore_homeassistant": True, + }, + ), + ( + { + "backup_id": "abc123", + "agent_id": "backup.local", + "password": "hunter2", + "restore_addons": ["addon_1"], + "restore_database": True, + "restore_folders": ["media"], + }, + { + "agent_id": "backup.local", + "password": "hunter2", + "restore_addons": ["addon_1"], + "restore_database": True, + "restore_folders": [backup.Folder.MEDIA], + "restore_homeassistant": True, + }, + ), + ( + { + "backup_id": "abc123", + "agent_id": "backup.local", + "password": "hunter2", + "restore_addons": ["addon_1", "addon_2"], + "restore_database": False, + "restore_folders": ["media", "share"], + }, + { + "agent_id": "backup.local", + "password": "hunter2", + "restore_addons": ["addon_1", "addon_2"], + "restore_database": False, + "restore_folders": [backup.Folder.MEDIA, backup.Folder.SHARE], + "restore_homeassistant": True, + }, + ), + ], +) +async def test_onboarding_backup_restore( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + params: dict[str, Any], + expected_kwargs: dict[str, Any], +) -> None: + """Test returning installation type during onboarding.""" + mock_storage(hass_storage, {"done": []}) + + assert await async_setup_component(hass, "onboarding", {}) + assert await async_setup_component(hass, "backup", {}) + await hass.async_block_till_done() + + client = await hass_client() + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_restore_backup", + ) as mock_restore: + resp = await client.post("/api/onboarding/backup/restore", json=params) + assert resp.status == 200 + mock_restore.assert_called_once_with("abc123", **expected_kwargs) + + +@pytest.mark.parametrize( + ("params", "restore_error", "expected_status", "expected_json", "restore_calls"), + [ + # Missing agent_id + ( + {"backup_id": "abc123"}, + None, + 400, + { + "message": "Message format incorrect: required key not provided @ data['agent_id']" + }, + 0, + ), + # Missing backup_id + ( + {"agent_id": "backup.local"}, + None, + 400, + { + "message": "Message format incorrect: required key not provided @ data['backup_id']" + }, + 0, + ), + # Invalid restore_database + ( + { + "backup_id": "abc123", + "agent_id": "backup.local", + "restore_database": "yes_please", + }, + None, + 400, + { + "message": "Message format incorrect: expected bool for dictionary value @ data['restore_database']" + }, + 0, + ), + # Invalid folder + ( + { + "backup_id": "abc123", + "agent_id": "backup.local", + "restore_folders": ["invalid"], + }, + None, + 400, + { + "message": "Message format incorrect: expected Folder or one of 'share', 'addons/local', 'ssl', 'media' @ data['restore_folders'][0]" + }, + 0, + ), + # Wrong password + ( + {"backup_id": "abc123", "agent_id": "backup.local"}, + backup.IncorrectPasswordError, + 400, + {"code": "incorrect_password"}, + 1, + ), + # Home Assistant error + ( + {"backup_id": "abc123", "agent_id": "backup.local"}, + HomeAssistantError("Boom!"), + 400, + {"code": "restore_failed", "message": "Boom!"}, + 1, + ), + ], +) +async def test_onboarding_backup_restore_error( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + params: dict[str, Any], + restore_error: Exception | None, + expected_status: int, + expected_json: str, + restore_calls: int, +) -> None: + """Test returning installation type during onboarding.""" + mock_storage(hass_storage, {"done": []}) + + assert await async_setup_component(hass, "onboarding", {}) + assert await async_setup_component(hass, "backup", {}) + await hass.async_block_till_done() + + client = await hass_client() + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_restore_backup", + side_effect=restore_error, + ) as mock_restore: + resp = await client.post("/api/onboarding/backup/restore", json=params) + + assert resp.status == expected_status + assert await resp.json() == expected_json + assert len(mock_restore.mock_calls) == restore_calls + + +@pytest.mark.parametrize( + ("params", "restore_error", "expected_status", "expected_message", "restore_calls"), + [ + # Unexpected error + ( + {"backup_id": "abc123", "agent_id": "backup.local"}, + Exception("Boom!"), + 500, + "500 Internal Server Error", + 1, + ), + ], +) +async def test_onboarding_backup_restore_unexpected_error( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + params: dict[str, Any], + restore_error: Exception | None, + expected_status: int, + expected_message: str, + restore_calls: int, +) -> None: + """Test returning installation type during onboarding.""" + mock_storage(hass_storage, {"done": []}) + + assert await async_setup_component(hass, "onboarding", {}) + assert await async_setup_component(hass, "backup", {}) + await hass.async_block_till_done() + + client = await hass_client() + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_restore_backup", + side_effect=restore_error, + ) as mock_restore: + resp = await client.post("/api/onboarding/backup/restore", json=params) + + assert resp.status == expected_status + assert (await resp.content.read()).decode().startswith(expected_message) + assert len(mock_restore.mock_calls) == restore_calls + + +async def test_onboarding_backup_upload( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, +) -> None: + """Test returning installation type during onboarding.""" + mock_storage(hass_storage, {"done": []}) + + assert await async_setup_component(hass, "onboarding", {}) + assert await async_setup_component(hass, "backup", {}) + await hass.async_block_till_done() + + client = await hass_client() + + with patch( + "homeassistant.components.backup.manager.BackupManager.async_receive_backup", + return_value="abc123", + ) as mock_receive: + resp = await client.post( + "/api/onboarding/backup/upload?agent_id=backup.local", + data={"file": StringIO("test")}, + ) + assert resp.status == 201 + assert await resp.json() == {"backup_id": "abc123"} + mock_receive.assert_called_once_with(agent_ids=["backup.local"], contents=ANY) diff --git a/tests/components/ondilo_ico/fixtures/pool2.json b/tests/components/ondilo_ico/fixtures/pool2.json index da0cb62d484..24e72b469f0 100644 --- a/tests/components/ondilo_ico/fixtures/pool2.json +++ b/tests/components/ondilo_ico/fixtures/pool2.json @@ -15,5 +15,5 @@ "latitude": 48.861783, "longitude": 2.337421 }, - "updated_at": "2024-01-01T01:00:00+0000" + "updated_at": "2024-01-01T01:05:00+0000" } diff --git a/tests/components/ondilo_ico/snapshots/test_init.ambr b/tests/components/ondilo_ico/snapshots/test_init.ambr index 44008ac907e..07e56a78fae 100644 --- a/tests/components/ondilo_ico/snapshots/test_init.ambr +++ b/tests/components/ondilo_ico/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/ondilo_ico/snapshots/test_sensor.ambr b/tests/components/ondilo_ico/snapshots/test_sensor.ambr index 56e30cd904a..84a2d3da4cb 100644 --- a/tests/components/ondilo_ico/snapshots/test_sensor.ambr +++ b/tests/components/ondilo_ico/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -109,6 +111,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -159,6 +162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -209,6 +213,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -259,6 +264,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -309,6 +315,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -360,6 +367,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -411,6 +419,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -461,6 +470,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -511,6 +521,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -561,6 +572,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -611,6 +623,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -661,6 +674,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ondilo_ico/test_init.py b/tests/components/ondilo_ico/test_init.py index 67f68f27b3e..58b1e27987d 100644 --- a/tests/components/ondilo_ico/test_init.py +++ b/tests/components/ondilo_ico/test_init.py @@ -1,8 +1,10 @@ """Test Ondilo ICO initialization.""" +from datetime import datetime, timedelta from typing import Any from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory from ondilo import OndiloError import pytest from syrupy import SnapshotAssertion @@ -13,7 +15,7 @@ from homeassistant.helpers import device_registry as dr from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_devices( @@ -63,6 +65,7 @@ async def test_get_pools_error( async def test_init_with_no_ico_attached( hass: HomeAssistant, mock_ondilo_client: MagicMock, + device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, pool1: dict[str, Any], ) -> None: @@ -73,14 +76,104 @@ async def test_init_with_no_ico_attached( mock_ondilo_client.get_ICO_details.return_value = None await setup_integration(hass, config_entry, mock_ondilo_client) + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + # No devices should be created + assert len(device_entries) == 0 # No sensor should be created assert len(hass.states.async_all()) == 0 # We should not have tried to retrieve pool measures mock_ondilo_client.get_last_pool_measures.assert_not_called() - assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert config_entry.state is ConfigEntryState.LOADED -@pytest.mark.parametrize("api", ["get_ICO_details", "get_last_pool_measures"]) +async def test_adding_pool_after_setup( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_ondilo_client: MagicMock, + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, + pool1: dict[str, Any], + two_pools: list[dict[str, Any]], + ico_details1: dict[str, Any], + ico_details2: dict[str, Any], +) -> None: + """Test adding one pool after integration setup.""" + mock_ondilo_client.get_pools.return_value = pool1 + mock_ondilo_client.get_ICO_details.return_value = ico_details1 + + await setup_integration(hass, config_entry, mock_ondilo_client) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + # One pool is created with 7 entities. + assert len(device_entries) == 1 + assert len(hass.states.async_all()) == 7 + + mock_ondilo_client.get_pools.return_value = two_pools + mock_ondilo_client.get_ICO_details.return_value = ico_details2 + + # Trigger a refresh of the pools coordinator. + freezer.tick(timedelta(minutes=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + # Two pool have been created with 7 entities each. + assert len(device_entries) == 2 + assert len(hass.states.async_all()) == 14 + + +async def test_removing_pool_after_setup( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_ondilo_client: MagicMock, + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, + pool1: dict[str, Any], + ico_details1: dict[str, Any], +) -> None: + """Test removing one pool after integration setup.""" + await setup_integration(hass, config_entry, mock_ondilo_client) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + # Two pools are created with 7 entities each. + assert len(device_entries) == 2 + assert len(hass.states.async_all()) == 14 + + mock_ondilo_client.get_pools.return_value = pool1 + mock_ondilo_client.get_ICO_details.return_value = ico_details1 + + # Trigger a refresh of the pools coordinator. + freezer.tick(timedelta(minutes=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + # One pool is left with 7 entities. + assert len(device_entries) == 1 + assert len(hass.states.async_all()) == 7 + + +@pytest.mark.parametrize( + ("api", "devices", "config_entry_state"), + [ + ("get_ICO_details", 0, ConfigEntryState.SETUP_RETRY), + ("get_last_pool_measures", 1, ConfigEntryState.LOADED), + ], +) async def test_details_error_all_pools( hass: HomeAssistant, mock_ondilo_client: MagicMock, @@ -88,6 +181,8 @@ async def test_details_error_all_pools( config_entry: MockConfigEntry, pool1: dict[str, Any], api: str, + devices: int, + config_entry_state: ConfigEntryState, ) -> None: """Test details and measures error for all pools.""" mock_ondilo_client.get_pools.return_value = pool1 @@ -100,8 +195,8 @@ async def test_details_error_all_pools( device_registry, config_entry.entry_id ) - assert not device_entries - assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert len(device_entries) == devices + assert config_entry.state is config_entry_state async def test_details_error_one_pool( @@ -131,12 +226,15 @@ async def test_details_error_one_pool( async def test_measures_error_one_pool( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, mock_ondilo_client: MagicMock, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, last_measures: list[dict[str, Any]], ) -> None: """Test measures error for one pool and success for the other.""" + entity_id_1 = "sensor.pool_1_temperature" + entity_id_2 = "sensor.pool_2_temperature" mock_ondilo_client.get_last_pool_measures.side_effect = [ OndiloError( 404, @@ -151,4 +249,170 @@ async def test_measures_error_one_pool( device_registry, config_entry.entry_id ) - assert len(device_entries) == 1 + assert len(device_entries) == 2 + # One pool returned an error, the other is ok. + # 7 entities are created for the second pool. + assert len(hass.states.async_all()) == 7 + assert hass.states.get(entity_id_1) is None + assert hass.states.get(entity_id_2) is not None + + # All pools now return measures. + mock_ondilo_client.get_last_pool_measures.side_effect = None + + # Move time to next pools coordinator refresh. + freezer.tick(timedelta(minutes=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + assert len(device_entries) == 2 + # 14 entities in total, 7 entities per pool. + assert len(hass.states.async_all()) == 14 + assert hass.states.get(entity_id_1) is not None + assert hass.states.get(entity_id_2) is not None + + +async def test_measures_scheduling( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_ondilo_client: MagicMock, + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, +) -> None: + """Test refresh scheduling of measures coordinator.""" + # Move time to 10 min after pool 1 was updated and 5 min after pool 2 was updated. + freezer.move_to("2024-01-01T01:10:00+00:00") + entity_id_1 = "sensor.pool_1_temperature" + entity_id_2 = "sensor.pool_2_temperature" + await setup_integration(hass, config_entry, mock_ondilo_client) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + + # Two pools are created with 7 entities each. + assert len(device_entries) == 2 + assert len(hass.states.async_all()) == 14 + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T01:10:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T01:10:00+00:00") + + # Tick time by 20 min. + # The measures coordinators for both pools should not have been refreshed again. + freezer.tick(timedelta(minutes=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T01:10:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T01:10:00+00:00") + + # Move time to 65 min after pool 1 was last updated. + # This is 5 min after we expect pool 1 to be updated again. + # The measures coordinator for pool 1 should refresh at this time. + # The measures coordinator for pool 2 should not have been refreshed again. + # The pools coordinator has updated the last update time + # of the pools to a stale time that is already passed. + freezer.move_to("2024-01-01T02:05:00+00:00") + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T02:05:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T01:10:00+00:00") + + # Tick time by 5 min. + # The measures coordinator for pool 1 should not have been refreshed again. + # The measures coordinator for pool 2 should refresh at this time. + # The pools coordinator has updated the last update time + # of the pools to a stale time that is already passed. + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T02:05:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T02:10:00+00:00") + + # Tick time by 55 min. + # The measures coordinator for pool 1 should refresh at this time. + # This is 1 hour after the last refresh of the measures coordinator for pool 1. + freezer.tick(timedelta(minutes=55)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T03:05:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T02:10:00+00:00") + + # Tick time by 5 min. + # The measures coordinator for pool 2 should refresh at this time. + # This is 1 hour after the last refresh of the measures coordinator for pool 2. + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T03:05:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T03:10:00+00:00") + + # Set an error on the pools coordinator endpoint. + # This will cause the pools coordinator to not update the next refresh. + # This should cause the measures coordinators to keep the 1 hour cadence. + mock_ondilo_client.get_pools.side_effect = OndiloError( + 502, + ( + " 502 Bad Gateway " + "

502 Bad Gateway

" + ), + ) + + # Tick time by 55 min. + # The measures coordinator for pool 1 should refresh at this time. + # This is 1 hour after the last refresh of the measures coordinator for pool 1. + freezer.tick(timedelta(minutes=55)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T04:05:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T03:10:00+00:00") + + # Tick time by 5 min. + # The measures coordinator for pool 2 should refresh at this time. + # This is 1 hour after the last refresh of the measures coordinator for pool 2. + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id_1) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T04:05:00+00:00") + state = hass.states.get(entity_id_2) + assert state is not None + assert state.last_reported == datetime.fromisoformat("2024-01-01T04:10:00+00:00") diff --git a/tests/components/onedrive/__init__.py b/tests/components/onedrive/__init__.py new file mode 100644 index 00000000000..0bafe37775b --- /dev/null +++ b/tests/components/onedrive/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the OneDrive integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the OneDrive integration for testing.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/onedrive/conftest.py b/tests/components/onedrive/conftest.py new file mode 100644 index 00000000000..ed419c820a9 --- /dev/null +++ b/tests/components/onedrive/conftest.py @@ -0,0 +1,138 @@ +"""Fixtures for OneDrive tests.""" + +from collections.abc import AsyncIterator, Generator +from json import dumps +import time +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.onedrive.const import DOMAIN, OAUTH_SCOPES +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import ( + BACKUP_METADATA, + CLIENT_ID, + CLIENT_SECRET, + MOCK_APPROOT, + MOCK_BACKUP_FILE, + MOCK_BACKUP_FOLDER, + MOCK_DRIVE, + MOCK_METADATA_FILE, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="scopes") +def mock_scopes() -> list[str]: + """Fixture to set the scopes present in the OAuth token.""" + return OAUTH_SCOPES + + +@pytest.fixture(autouse=True) +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + +@pytest.fixture(name="expires_at") +def mock_expires_at() -> int: + """Fixture to set the oauth token expiration time.""" + return time.time() + 3600 + + +@pytest.fixture +def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="John Doe's OneDrive", + domain=DOMAIN, + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": expires_at, + "scope": " ".join(scopes), + }, + }, + unique_id="mock_drive_id", + ) + + +@pytest.fixture +def mock_onedrive_client_init() -> Generator[MagicMock]: + """Return a mocked GraphServiceClient.""" + with ( + patch( + "homeassistant.components.onedrive.config_flow.OneDriveClient", + autospec=True, + ) as onedrive_client, + patch( + "homeassistant.components.onedrive.OneDriveClient", + new=onedrive_client, + ), + ): + yield onedrive_client + + +@pytest.fixture(autouse=True) +def mock_onedrive_client(mock_onedrive_client_init: MagicMock) -> Generator[MagicMock]: + """Return a mocked GraphServiceClient.""" + client = mock_onedrive_client_init.return_value + client.get_approot.return_value = MOCK_APPROOT + client.create_folder.return_value = MOCK_BACKUP_FOLDER + client.list_drive_items.return_value = [MOCK_BACKUP_FILE, MOCK_METADATA_FILE] + client.get_drive_item.return_value = MOCK_BACKUP_FILE + client.upload_file.return_value = MOCK_METADATA_FILE + + class MockStreamReader: + async def iter_chunked(self, chunk_size: int) -> AsyncIterator[bytes]: + yield b"backup data" + + async def read(self) -> bytes: + return dumps(BACKUP_METADATA).encode() + + client.download_drive_item.return_value = MockStreamReader() + client.get_drive.return_value = MOCK_DRIVE + return client + + +@pytest.fixture +def mock_large_file_upload_client() -> Generator[AsyncMock]: + """Return a mocked LargeFileUploadClient upload.""" + with patch( + "homeassistant.components.onedrive.backup.LargeFileUploadClient.upload" + ) as mock_upload: + mock_upload.return_value = MOCK_BACKUP_FILE + yield mock_upload + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.onedrive.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(autouse=True) +def mock_instance_id() -> Generator[AsyncMock]: + """Mock the instance ID.""" + with patch( + "homeassistant.components.onedrive.async_get_instance_id", + return_value="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0", + ): + yield diff --git a/tests/components/onedrive/const.py b/tests/components/onedrive/const.py new file mode 100644 index 00000000000..44f50aa625d --- /dev/null +++ b/tests/components/onedrive/const.py @@ -0,0 +1,118 @@ +"""Consts for OneDrive tests.""" + +from html import escape +from json import dumps + +from onedrive_personal_sdk.const import DriveState, DriveType +from onedrive_personal_sdk.models.items import ( + AppRoot, + Drive, + DriveQuota, + File, + Folder, + Hashes, + IdentitySet, + ItemParentReference, + User, +) + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +BACKUP_METADATA = { + "addons": [], + "backup_id": "23e64aec", + "date": "2024-11-22T11:48:48.727189+01:00", + "database_included": True, + "extra_metadata": {}, + "folders": [], + "homeassistant_included": True, + "homeassistant_version": "2024.12.0.dev0", + "name": "Core 2024.12.0.dev0", + "protected": False, + "size": 34519040, +} + +IDENTITY_SET = IdentitySet( + user=User( + display_name="John Doe", + id="id", + email="john@doe.com", + ) +) + +MOCK_APPROOT = AppRoot( + id="id", + child_count=0, + size=0, + name="name", + parent_reference=ItemParentReference( + drive_id="mock_drive_id", id="id", path="path" + ), + created_by=IDENTITY_SET, +) + +MOCK_BACKUP_FOLDER = Folder( + id="id", + name="name", + size=0, + child_count=0, + parent_reference=ItemParentReference( + drive_id="mock_drive_id", id="id", path="path" + ), + created_by=IDENTITY_SET, +) + +MOCK_BACKUP_FILE = File( + id="id", + name="23e64aec.tar", + size=34519040, + parent_reference=ItemParentReference( + drive_id="mock_drive_id", id="id", path="path" + ), + hashes=Hashes( + quick_xor_hash="hash", + ), + mime_type="application/x-tar", + description="", + created_by=IDENTITY_SET, +) + +MOCK_METADATA_FILE = File( + id="id", + name="23e64aec.tar", + size=34519040, + parent_reference=ItemParentReference( + drive_id="mock_drive_id", id="id", path="path" + ), + hashes=Hashes( + quick_xor_hash="hash", + ), + mime_type="application/x-tar", + description=escape( + dumps( + { + "metadata_version": 2, + "backup_id": "23e64aec", + "backup_file_id": "id", + } + ) + ), + created_by=IDENTITY_SET, +) + + +MOCK_DRIVE = Drive( + id="mock_drive_id", + name="My Drive", + drive_type=DriveType.PERSONAL, + owner=IDENTITY_SET, + quota=DriveQuota( + deleted=5, + remaining=750000000, + state=DriveState.NEARING, + total=5000000000, + used=4250000000, + ), +) diff --git a/tests/components/onedrive/snapshots/test_init.ambr b/tests/components/onedrive/snapshots/test_init.ambr new file mode 100644 index 00000000000..9b2ed7e4d94 --- /dev/null +++ b/tests/components/onedrive/snapshots/test_init.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_device + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://onedrive.live.com/?id=root&cid=mock_drive_id', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onedrive', + 'mock_drive_id', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Microsoft', + 'model': 'OneDrive Personal', + 'model_id': None, + 'name': 'My Drive', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/onedrive/snapshots/test_sensor.ambr b/tests/components/onedrive/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..43c6921b0e5 --- /dev/null +++ b/tests/components/onedrive/snapshots/test_sensor.ambr @@ -0,0 +1,227 @@ +# serializer version: 1 +# name: test_sensors[sensor.my_drive_drive_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'nearing', + 'critical', + 'exceeded', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_drive_drive_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Drive state', + 'platform': 'onedrive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state', + 'unique_id': 'mock_drive_id_drive_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.my_drive_drive_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'My Drive Drive state', + 'options': list([ + 'normal', + 'nearing', + 'critical', + 'exceeded', + ]), + }), + 'context': , + 'entity_id': 'sensor.my_drive_drive_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'nearing', + }) +# --- +# name: test_sensors[sensor.my_drive_remaining_storage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_drive_remaining_storage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Remaining storage', + 'platform': 'onedrive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_size', + 'unique_id': 'mock_drive_id_remaining_size', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.my_drive_remaining_storage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'My Drive Remaining storage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_drive_remaining_storage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.75', + }) +# --- +# name: test_sensors[sensor.my_drive_total_available_storage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_drive_total_available_storage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total available storage', + 'platform': 'onedrive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_size', + 'unique_id': 'mock_drive_id_total_size', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.my_drive_total_available_storage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'My Drive Total available storage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_drive_total_available_storage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[sensor.my_drive_used_storage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_drive_used_storage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Used storage', + 'platform': 'onedrive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'used_size', + 'unique_id': 'mock_drive_id_used_size', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.my_drive_used_storage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'My Drive Used storage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_drive_used_storage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.25', + }) +# --- diff --git a/tests/components/onedrive/test_backup.py b/tests/components/onedrive/test_backup.py new file mode 100644 index 00000000000..41ecbdb240f --- /dev/null +++ b/tests/components/onedrive/test_backup.py @@ -0,0 +1,386 @@ +"""Test the backups for OneDrive.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from io import StringIO +from unittest.mock import Mock, patch + +from onedrive_personal_sdk.exceptions import ( + AuthenticationError, + HashMismatchError, + OneDriveException, +) +import pytest + +from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup +from homeassistant.components.onedrive.backup import ( + async_register_backup_agents_listener, +) +from homeassistant.components.onedrive.const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import setup_integration +from .const import BACKUP_METADATA, MOCK_BACKUP_FILE, MOCK_METADATA_FILE + +from tests.common import AsyncMock, MockConfigEntry +from tests.typing import ClientSessionGenerator, MagicMock, WebSocketGenerator + + +@pytest.fixture(autouse=True) +async def setup_backup_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> AsyncGenerator[None]: + """Set up onedrive integration.""" + with ( + patch("homeassistant.components.backup.is_hassio", return_value=False), + patch("homeassistant.components.backup.store.STORE_DELAY_SAVE", 0), + ): + assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}}) + await setup_integration(hass, mock_config_entry) + + await hass.async_block_till_done() + yield + + +async def test_agents_info( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, +) -> None: + """Test backup agent info.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/agents/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agents": [ + {"agent_id": "backup.local", "name": "local"}, + { + "agent_id": f"{DOMAIN}.{mock_config_entry.unique_id}", + "name": mock_config_entry.title, + }, + ], + } + + +async def test_agents_list_backups( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, +) -> None: + """Test agent list backups.""" + + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "backup/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["agent_errors"] == {} + assert response["result"]["backups"] == [ + { + "addons": [], + "agents": { + "onedrive.mock_drive_id": {"protected": False, "size": 34519040} + }, + "backup_id": "23e64aec", + "date": "2024-11-22T11:48:48.727189+01:00", + "database_included": True, + "extra_metadata": {}, + "folders": [], + "homeassistant_included": True, + "homeassistant_version": "2024.12.0.dev0", + "name": "Core 2024.12.0.dev0", + "failed_agent_ids": [], + "with_automatic_settings": None, + } + ] + + +async def test_agents_get_backup( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_config_entry: MockConfigEntry, +) -> None: + """Test agent get backup.""" + + backup_id = BACKUP_METADATA["backup_id"] + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["agent_errors"] == {} + assert response["result"]["backup"] == { + "addons": [], + "agents": { + f"{DOMAIN}.{mock_config_entry.unique_id}": { + "protected": False, + "size": 34519040, + } + }, + "backup_id": "23e64aec", + "date": "2024-11-22T11:48:48.727189+01:00", + "database_included": True, + "extra_metadata": {}, + "folders": [], + "homeassistant_included": True, + "homeassistant_version": "2024.12.0.dev0", + "name": "Core 2024.12.0.dev0", + "failed_agent_ids": [], + "with_automatic_settings": None, + } + + +async def test_agents_delete( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_onedrive_client: MagicMock, +) -> None: + """Test agent delete backup.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "backup/delete", + "backup_id": BACKUP_METADATA["backup_id"], + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"agent_errors": {}} + assert mock_onedrive_client.delete_drive_item.call_count == 2 + + +async def test_agents_upload( + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, + mock_onedrive_client: MagicMock, + mock_large_file_upload_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test agent upload backup.""" + client = await hass_client() + test_backup = AgentBackup.from_dict(BACKUP_METADATA) + + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) + fetch_backup.return_value = test_backup + resp = await client.post( + f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.unique_id}", + data={"file": StringIO("test")}, + ) + + assert resp.status == 201 + assert f"Uploading backup {test_backup.backup_id}" in caplog.text + mock_large_file_upload_client.assert_called_once() + mock_onedrive_client.update_drive_item.assert_called_once() + + +async def test_agents_upload_corrupt_upload( + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, + mock_onedrive_client: MagicMock, + mock_large_file_upload_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test hash validation fails.""" + mock_large_file_upload_client.side_effect = HashMismatchError("test") + client = await hass_client() + test_backup = AgentBackup.from_dict(BACKUP_METADATA) + + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) + fetch_backup.return_value = test_backup + resp = await client.post( + f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.unique_id}", + data={"file": StringIO("test")}, + ) + + assert resp.status == 201 + assert f"Uploading backup {test_backup.backup_id}" in caplog.text + mock_large_file_upload_client.assert_called_once() + assert mock_onedrive_client.update_drive_item.call_count == 0 + assert "Hash validation failed, backup file might be corrupt" in caplog.text + + +async def test_agents_download( + hass_client: ClientSessionGenerator, + mock_onedrive_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test agent download backup.""" + client = await hass_client() + backup_id = BACKUP_METADATA["backup_id"] + + resp = await client.get( + f"/api/backup/download/{backup_id}?agent_id={DOMAIN}.{mock_config_entry.unique_id}" + ) + assert resp.status == 200 + assert await resp.content.read() == b"backup data" + + +async def test_error_on_agents_download( + hass_client: ClientSessionGenerator, + mock_onedrive_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we get not found on an not existing backup on download.""" + client = await hass_client() + backup_id = BACKUP_METADATA["backup_id"] + mock_onedrive_client.list_drive_items.side_effect = [ + [MOCK_BACKUP_FILE, MOCK_METADATA_FILE], + [], + ] + + with patch("homeassistant.components.onedrive.backup.CACHE_TTL", -1): + resp = await client.get( + f"/api/backup/download/{backup_id}?agent_id={DOMAIN}.{mock_config_entry.unique_id}" + ) + assert resp.status == 404 + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + ( + OneDriveException(), + "Backup operation failed", + ), + (TimeoutError(), "Backup operation timed out"), + ], +) +async def test_delete_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_onedrive_client: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: + """Test error during delete.""" + mock_onedrive_client.delete_drive_item.side_effect = AsyncMock( + side_effect=side_effect + ) + + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "backup/delete", + "backup_id": BACKUP_METADATA["backup_id"], + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agent_errors": {f"{DOMAIN}.{mock_config_entry.unique_id}": error} + } + + +async def test_agents_delete_not_found_does_not_throw( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_onedrive_client: MagicMock, +) -> None: + """Test agent delete backup.""" + mock_onedrive_client.list_drive_items.return_value = [] + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "backup/delete", + "backup_id": BACKUP_METADATA["backup_id"], + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"agent_errors": {}} + + +async def test_agents_backup_not_found( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_onedrive_client: MagicMock, +) -> None: + """Test backup not found.""" + + mock_onedrive_client.list_drive_items.return_value = [] + backup_id = BACKUP_METADATA["backup_id"] + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["backup"] is None + + +async def test_reauth_on_403( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + mock_onedrive_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we re-authenticate on 403.""" + + mock_onedrive_client.list_drive_items.side_effect = AuthenticationError( + 403, "Auth failed" + ) + backup_id = BACKUP_METADATA["backup_id"] + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["agent_errors"] == { + f"{DOMAIN}.{mock_config_entry.unique_id}": "Authentication error" + } + + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + +async def test_listeners_get_cleaned_up(hass: HomeAssistant) -> None: + """Test listener gets cleaned up.""" + listener = MagicMock() + remove_listener = async_register_backup_agents_listener(hass, listener=listener) + + # make sure it's the last listener + hass.data[DATA_BACKUP_AGENT_LISTENERS] = [listener] + remove_listener() + + assert hass.data.get(DATA_BACKUP_AGENT_LISTENERS) is None diff --git a/tests/components/onedrive/test_config_flow.py b/tests/components/onedrive/test_config_flow.py new file mode 100644 index 00000000000..fb0d58b86c6 --- /dev/null +++ b/tests/components/onedrive/test_config_flow.py @@ -0,0 +1,225 @@ +"""Test the OneDrive config flow.""" + +from http import HTTPStatus +from unittest.mock import AsyncMock, MagicMock + +from onedrive_personal_sdk.exceptions import OneDriveException +import pytest + +from homeassistant import config_entries +from homeassistant.components.onedrive.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow + +from . import setup_integration +from .const import CLIENT_ID, MOCK_APPROOT + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker +from tests.typing import ClientSessionGenerator + + +async def _do_get_token( + hass: HomeAssistant, + result: ConfigFlowResult, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, +) -> None: + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + scope = "Files.ReadWrite.AppFolder+offline_access+openid" + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={scope}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_setup_entry: AsyncMock, + mock_onedrive_client_init: MagicMock, +) -> None: + """Check full flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + # Ensure the token callback is set up correctly + token_callback = mock_onedrive_client_init.call_args[0][0] + assert await token_callback() == "mock-access-token" + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["title"] == "John Doe's OneDrive" + assert result["result"].unique_id == "mock_drive_id" + assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token" + assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token" + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_full_flow_with_owner_not_found( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_setup_entry: AsyncMock, + mock_onedrive_client: MagicMock, +) -> None: + """Ensure we get a default title if the drive's owner can't be read.""" + + mock_onedrive_client.get_approot.return_value.created_by.user = None + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result["title"] == "OneDrive" + assert result["result"].unique_id == "mock_drive_id" + assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token" + assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token" + + +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.parametrize( + ("exception", "error"), + [ + (Exception, "unknown"), + (OneDriveException, "connection_error"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_onedrive_client: MagicMock, + exception: Exception, + error: str, +) -> None: + """Test errors during flow.""" + + mock_onedrive_client.get_approot.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == error + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_already_configured( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test already configured account.""" + await setup_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_reauth_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the reauth flow works.""" + + await setup_integration(hass, mock_config_entry) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_reauth_flow_id_changed( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, +) -> None: + """Test that the reauth flow fails on a different drive id.""" + app_root = MOCK_APPROOT + app_root.parent_reference.drive_id = "other_drive_id" + mock_onedrive_client.get_approot.return_value = app_root + + await setup_integration(hass, mock_config_entry) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_drive" diff --git a/tests/components/onedrive/test_init.py b/tests/components/onedrive/test_init.py new file mode 100644 index 00000000000..b4ec138ebf4 --- /dev/null +++ b/tests/components/onedrive/test_init.py @@ -0,0 +1,170 @@ +"""Test the OneDrive setup.""" + +from copy import deepcopy +from html import escape +from json import dumps +from unittest.mock import MagicMock + +from onedrive_personal_sdk.const import DriveState +from onedrive_personal_sdk.exceptions import AuthenticationError, OneDriveException +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.onedrive.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, issue_registry as ir + +from . import setup_integration +from .const import BACKUP_METADATA, MOCK_BACKUP_FILE, MOCK_DRIVE + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client_init: MagicMock, + mock_onedrive_client: MagicMock, +) -> None: + """Test loading and unloading the integration.""" + await setup_integration(hass, mock_config_entry) + + # Ensure the token callback is set up correctly + token_callback = mock_onedrive_client_init.call_args[0][0] + assert await token_callback() == "mock-access-token" + + # make sure metadata migration is not called + assert mock_onedrive_client.upload_file.call_count == 0 + assert mock_onedrive_client.update_drive_item.call_count == 0 + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("side_effect", "state"), + [ + (AuthenticationError(403, "Auth failed"), ConfigEntryState.SETUP_ERROR), + (OneDriveException(), ConfigEntryState.SETUP_RETRY), + ], +) +async def test_approot_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, + side_effect: Exception, + state: ConfigEntryState, +) -> None: + """Test errors during approot retrieval.""" + mock_onedrive_client.get_approot.side_effect = side_effect + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is state + + +async def test_get_integration_folder_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test faulty approot retrieval.""" + mock_onedrive_client.create_folder.side_effect = OneDriveException() + await setup_integration(hass, mock_config_entry) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert "Failed to get backups_9f86d081 folder" in caplog.text + + +async def test_migrate_metadata_files( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, +) -> None: + """Test migration of metadata files.""" + MOCK_BACKUP_FILE.description = escape( + dumps({**BACKUP_METADATA, "metadata_version": 1}) + ) + await setup_integration(hass, mock_config_entry) + await hass.async_block_till_done() + + mock_onedrive_client.upload_file.assert_called_once() + assert mock_onedrive_client.update_drive_item.call_count == 2 + assert mock_onedrive_client.update_drive_item.call_args[1]["data"].description == "" + + +async def test_migrate_metadata_files_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, +) -> None: + """Test migration of metadata files errors.""" + mock_onedrive_client.list_drive_items.side_effect = OneDriveException() + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_auth_error_during_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, +) -> None: + """Test auth error during update.""" + mock_onedrive_client.get_drive.side_effect = AuthenticationError(403, "Auth failed") + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the device.""" + + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_device({(DOMAIN, MOCK_DRIVE.id)}) + assert device + assert device == snapshot + + +@pytest.mark.parametrize( + ( + "drive_state", + "issue_key", + "issue_exists", + ), + [ + (DriveState.NORMAL, "drive_full", False), + (DriveState.NORMAL, "drive_almost_full", False), + (DriveState.CRITICAL, "drive_almost_full", True), + (DriveState.CRITICAL, "drive_full", False), + (DriveState.EXCEEDED, "drive_almost_full", False), + (DriveState.EXCEEDED, "drive_full", True), + ], +) +async def test_data_cap_issues( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, + drive_state: DriveState, + issue_key: str, + issue_exists: bool, +) -> None: + """Make sure we get issues for high data usage.""" + mock_drive = deepcopy(MOCK_DRIVE) + assert mock_drive.quota + mock_drive.quota.state = drive_state + mock_onedrive_client.get_drive.return_value = mock_drive + await setup_integration(hass, mock_config_entry) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(DOMAIN, issue_key) + assert (issue is not None) == issue_exists diff --git a/tests/components/onedrive/test_sensor.py b/tests/components/onedrive/test_sensor.py new file mode 100644 index 00000000000..ea9d93a9a7b --- /dev/null +++ b/tests/components/onedrive/test_sensor.py @@ -0,0 +1,64 @@ +"""Tests for OneDrive sensors.""" + +from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +from onedrive_personal_sdk.const import DriveType +from onedrive_personal_sdk.exceptions import HttpRequestException +from onedrive_personal_sdk.models.items import Drive +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the OneDrive sensors.""" + + await setup_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("attr", "side_effect"), + [ + ("side_effect", HttpRequestException(503, "Service Unavailable")), + ("return_value", Drive(id="id", name="name", drive_type=DriveType.PERSONAL)), + ], +) +async def test_update_failure( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_onedrive_client: MagicMock, + freezer: FrozenDateTimeFactory, + attr: str, + side_effect: Any, +) -> None: + """Ensure sensors are going unavailable on update failure.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.my_drive_remaining_storage") + assert state.state == "0.75" + + setattr(mock_onedrive_client.get_drive, attr, side_effect) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.my_drive_remaining_storage") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index ed15cac94be..595b660b722 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -7,57 +7,60 @@ from unittest.mock import MagicMock from pyownet.protocol import ProtocolError -from homeassistant.const import Platform - from .const import ATTR_INJECT_READS, MOCK_OWPROXY_DEVICES -def setup_owproxy_mock_devices( - owproxy: MagicMock, platform: Platform, device_ids: list[str] -) -> None: +def setup_owproxy_mock_devices(owproxy: MagicMock, device_ids: list[str]) -> None: """Set up mock for owproxy.""" - main_dir_return_value = [] - sub_dir_side_effect = [] - main_read_side_effect = [] - sub_read_side_effect = [] + dir_side_effect: dict[str, list] = {} + read_side_effect: dict[str, list] = { + "/system/configuration/version": [b"3.2"], + } + + # Setup directory listing + dir_side_effect["/"] = [[f"/{device_id}/" for device_id in device_ids]] for device_id in device_ids: - _setup_owproxy_mock_device( - main_dir_return_value, - sub_dir_side_effect, - main_read_side_effect, - sub_read_side_effect, - device_id, - platform, - ) + _setup_owproxy_mock_device(dir_side_effect, read_side_effect, device_id) - # Ensure enough read side effect - dir_side_effect = [main_dir_return_value, *sub_dir_side_effect] - read_side_effect = ( - main_read_side_effect - + sub_read_side_effect - + [ProtocolError("Missing injected value")] * 20 - ) - owproxy.return_value.dir.side_effect = dir_side_effect - owproxy.return_value.read.side_effect = read_side_effect + def _dir(path: str) -> Any: + if (side_effect := dir_side_effect.get(path)) is None: + raise NotImplementedError(f"Unexpected _dir call: {path}") + result = side_effect.pop(0) + if isinstance(result, Exception) or ( + isinstance(result, type) and issubclass(result, Exception) + ): + raise result + return result + + def _read(path: str) -> Any: + if (side_effect := read_side_effect.get(path)) is None: + raise NotImplementedError(f"Unexpected _read call: {path}") + if len(side_effect) == 0: + raise ProtocolError(f"Missing injected value for: {path}") + result = side_effect.pop(0) + if isinstance(result, Exception) or ( + isinstance(result, type) and issubclass(result, Exception) + ): + raise result + return result + + owproxy.return_value.dir.side_effect = _dir + owproxy.return_value.read.side_effect = _read def _setup_owproxy_mock_device( - main_dir_return_value: list, - sub_dir_side_effect: list, - main_read_side_effect: list, - sub_read_side_effect: list, - device_id: str, - platform: Platform, + dir_side_effect: dict[str, list], read_side_effect: dict[str, list], device_id: str ) -> None: """Set up mock for owproxy.""" mock_device = MOCK_OWPROXY_DEVICES[device_id] - # Setup directory listing - main_dir_return_value += [f"/{device_id}/"] if "branches" in mock_device: # Setup branch directory listing for branch, branch_details in mock_device["branches"].items(): + sub_dir_side_effect = dir_side_effect.setdefault( + f"/{device_id}/{branch}", [] + ) sub_dir_side_effect.append( [ # dir on branch f"/{device_id}/{branch}/{sub_device_id}/" @@ -65,46 +68,31 @@ def _setup_owproxy_mock_device( ] ) - _setup_owproxy_mock_device_reads( - main_read_side_effect, - sub_read_side_effect, - mock_device, - device_id, - platform, - ) + _setup_owproxy_mock_device_reads(read_side_effect, mock_device, "/", device_id) if "branches" in mock_device: - for branch_details in mock_device["branches"].values(): + for branch, branch_details in mock_device["branches"].items(): for sub_device_id, sub_device in branch_details.items(): _setup_owproxy_mock_device_reads( - main_read_side_effect, - sub_read_side_effect, + read_side_effect, sub_device, + f"/{device_id}/{branch}/", sub_device_id, - platform, ) def _setup_owproxy_mock_device_reads( - main_read_side_effect: list, - sub_read_side_effect: list, - mock_device: Any, - device_id: str, - platform: Platform, + read_side_effect: dict[str, list], mock_device: Any, root_path: str, device_id: str ) -> None: """Set up mock for owproxy.""" # Setup device reads - main_read_side_effect += [device_id[0:2].encode()] - if ATTR_INJECT_READS in mock_device: - main_read_side_effect += mock_device[ATTR_INJECT_READS] - - # Setup sub-device reads - device_sensors = mock_device.get(platform, []) - if platform is Platform.SENSOR and device_id.startswith("12"): - # We need to check if there is TAI8570 plugged in - sub_read_side_effect.extend( - expected_sensor[ATTR_INJECT_READS] for expected_sensor in device_sensors - ) - sub_read_side_effect.extend( - expected_sensor[ATTR_INJECT_READS] for expected_sensor in device_sensors + family_read_side_effect = read_side_effect.setdefault( + f"{root_path}{device_id}/family", [] ) + family_read_side_effect += [device_id[0:2].encode()] + if ATTR_INJECT_READS in mock_device: + for k, v in mock_device[ATTR_INJECT_READS].items(): + device_read_side_effect = read_side_effect.setdefault( + f"{root_path}{device_id}{k}", [] + ) + device_read_side_effect += v diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 65a86b58f2f..9d4303eaa1c 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -7,7 +7,7 @@ from pyownet.protocol import ConnError import pytest from homeassistant.components.onewire.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant @@ -32,7 +32,7 @@ def get_device_id(request: pytest.FixtureRequest) -> str: @pytest.fixture(name="config_entry") -def get_config_entry(hass: HomeAssistant) -> ConfigEntry: +def get_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Create and register mock config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -54,14 +54,14 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: @pytest.fixture(name="owproxy") -def get_owproxy() -> MagicMock: +def get_owproxy() -> Generator[MagicMock]: """Mock owproxy.""" with patch("homeassistant.components.onewire.onewirehub.protocol.proxy") as owproxy: yield owproxy @pytest.fixture(name="owproxy_with_connerror") -def get_owproxy_with_connerror() -> MagicMock: +def get_owproxy_with_connerror() -> Generator[MagicMock]: """Mock owproxy.""" with patch( "homeassistant.components.onewire.onewirehub.protocol.proxy", diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index a1bab9807d5..370bcc871c6 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -2,333 +2,256 @@ from pyownet.protocol import ProtocolError -from homeassistant.components.onewire.const import Platform - -ATTR_DEVICE_FILE = "device_file" ATTR_INJECT_READS = "inject_reads" MOCK_OWPROXY_DEVICES = { "00.111111111111": { - ATTR_INJECT_READS: [ - b"", # read device type - ], + ATTR_INJECT_READS: { + "/type": [b""], + }, }, "05.111111111111": { - ATTR_INJECT_READS: [ - b"DS2405", # read device type - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b" 1"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2405"], + "/PIO": [b" 1"], + }, }, "10.111111111111": { - ATTR_INJECT_READS: [ - b"DS18S20", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 25.123"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS18S20"], + "/temperature": [b" 25.123"], + }, }, "12.111111111111": { - ATTR_INJECT_READS: [ - b"DS2406", # read device type - ], - Platform.BINARY_SENSOR: [ - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 25.123"}, - {ATTR_INJECT_READS: b" 1025.123"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2406"], + # TAI8570 values are read twice: + # - once during init to make sure TAI8570 is accessible + # - once during first update to get the actual values + "/TAI8570/temperature": [b" 25.123", b" 25.123"], + "/TAI8570/pressure": [b" 1025.123", b" 1025.123"], + "/PIO.A": [b" 1"], + "/PIO.B": [b" 0"], + "/latch.A": [b" 1"], + "/latch.B": [b" 0"], + "/sensed.A": [b" 1"], + "/sensed.B": [b" 0"], + }, }, "1D.111111111111": { - ATTR_INJECT_READS: [ - b"DS2423", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 251123"}, - {ATTR_INJECT_READS: b" 248125"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2423"], + "/counter.A": [b" 251123"], + "/counter.B": [b" 248125"], + } }, "16.111111111111": { # Test case for issue #115984, where the device type cannot be read - ATTR_INJECT_READS: [ - ProtocolError(), # read device type - ], + ATTR_INJECT_READS: {"/type": [ProtocolError()]}, }, "1F.111111111111": { - ATTR_INJECT_READS: [ - b"DS2409", # read device type - ], + ATTR_INJECT_READS: {"/type": [b"DS2409"]}, "branches": { "aux": {}, "main": { "1D.111111111111": { - ATTR_INJECT_READS: [ - b"DS2423", # read device type - ], - Platform.SENSOR: [ - { - ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.A", - ATTR_INJECT_READS: b" 251123", - }, - { - ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.B", - ATTR_INJECT_READS: b" 248125", - }, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2423"], + "/counter.A": [b" 251123"], + "/counter.B": [b" 248125"], + }, }, }, }, }, + "20.111111111111": { + ATTR_INJECT_READS: { + "/type": [b"DS2450"], + "/volt.A": [b" 1.1"], + "/volt.B": [b" 2.2"], + "/volt.C": [b" 3.3"], + "/volt.D": [b" 4.4"], + "/latestvolt.A": [b" 1.11"], + "/latestvolt.B": [b" 2.22"], + "/latestvolt.C": [b" 3.33"], + "/latestvolt.D": [b" 4.44"], + } + }, "22.111111111111": { - ATTR_INJECT_READS: [ - b"DS1822", # read device type - ], - Platform.SENSOR: [ - { - ATTR_INJECT_READS: ProtocolError, - }, - ], + ATTR_INJECT_READS: { + "/type": [b"DS1822"], + "/temperature": [ProtocolError], + }, }, "26.111111111111": { - ATTR_INJECT_READS: [ - b"DS2438", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 25.123"}, - {ATTR_INJECT_READS: b" 72.7563"}, - {ATTR_INJECT_READS: b" 73.7563"}, - {ATTR_INJECT_READS: b" 74.7563"}, - {ATTR_INJECT_READS: b" 75.7563"}, - { - ATTR_INJECT_READS: ProtocolError, - }, - {ATTR_INJECT_READS: b" 969.265"}, - {ATTR_INJECT_READS: b" 65.8839"}, - {ATTR_INJECT_READS: b" 2.97"}, - {ATTR_INJECT_READS: b" 4.74"}, - {ATTR_INJECT_READS: b" 0.12"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b" 1"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2438"], + "/temperature": [b" 25.123"], + "/humidity": [b" 72.7563"], + "/HIH3600/humidity": [b" 73.7563"], + "/HIH4000/humidity": [b" 74.7563"], + "/HIH5030/humidity": [b" 75.7563"], + "/HTM1735/humidity": [ProtocolError], + "/B1-R1-A/pressure": [b" 969.265"], + "/S3-R1-A/illuminance": [b" 65.8839"], + "/VAD": [b" 2.97"], + "/VDD": [b" 4.74"], + "/vis": [b" 0.12"], + "/IAD": [b" 1"], + }, }, "28.111111111111": { - ATTR_INJECT_READS: [ - b"DS18B20", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 26.984"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS18B20"], + "/temperature": [b" 26.984"], + "/tempres": [b" 12"], + }, }, "28.222222222222": { # This device has precision options in the config entry - ATTR_INJECT_READS: [ - b"DS18B20", # read device type - ], - Platform.SENSOR: [ - { - ATTR_DEVICE_FILE: "/28.222222222222/temperature9", - ATTR_INJECT_READS: b" 26.984", - }, - ], + ATTR_INJECT_READS: { + "/type": [b"DS18B20"], + "/temperature9": [b" 26.984"], + }, }, "28.222222222223": { # This device has an illegal precision option in the config entry - ATTR_INJECT_READS: [ - b"DS18B20", # read device type - ], - Platform.SENSOR: [ - { - ATTR_DEVICE_FILE: "/28.222222222223/temperature", - ATTR_INJECT_READS: b" 26.984", - }, - ], + ATTR_INJECT_READS: { + "/type": [b"DS18B20"], + "/temperature": [b" 26.984"], + }, }, "29.111111111111": { - ATTR_INJECT_READS: [ - b"DS2408", # read device type - ], - Platform.BINARY_SENSOR: [ - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, - { - ATTR_INJECT_READS: ProtocolError, - }, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - { - ATTR_INJECT_READS: ProtocolError, - }, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2408"], + "/PIO.0": [b" 1"], + "/PIO.1": [b" 0"], + "/PIO.2": [b" 1"], + "/PIO.3": [ProtocolError], + "/PIO.4": [b" 1"], + "/PIO.5": [b" 0"], + "/PIO.6": [b" 1"], + "/PIO.7": [b" 0"], + "/latch.0": [b" 1"], + "/latch.1": [b" 0"], + "/latch.2": [b" 1"], + "/latch.3": [b" 0"], + "/latch.4": [b" 1"], + "/latch.5": [b" 0"], + "/latch.6": [b" 1"], + "/latch.7": [b" 0"], + "/sensed.0": [b" 1"], + "/sensed.1": [b" 0"], + "/sensed.2": [b" 0"], + "/sensed.3": [ProtocolError], + "/sensed.4": [b" 0"], + "/sensed.5": [b" 0"], + "/sensed.6": [b" 0"], + "/sensed.7": [b" 0"], + }, }, "30.111111111111": { - ATTR_INJECT_READS: [ - b"DS2760", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 26.984"}, - { - ATTR_DEVICE_FILE: "/30.111111111111/typeK/temperature", - ATTR_INJECT_READS: b" 173.7563", - }, - {ATTR_INJECT_READS: b" 2.97"}, - {ATTR_INJECT_READS: b" 0.12"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2760"], + "/temperature": [b" 26.984"], + "/typeK/temperature": [b" 173.7563"], + "/volt": [b" 2.97"], + "/vis": [b" 0.12"], + }, }, "3A.111111111111": { - ATTR_INJECT_READS: [ - b"DS2413", # read device type - ], - Platform.BINARY_SENSOR: [ - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2413"], + "/PIO.A": [b" 1"], + "/PIO.B": [b" 0"], + "/sensed.A": [b" 1"], + "/sensed.B": [b" 0"], + }, }, "3B.111111111111": { - ATTR_INJECT_READS: [ - b"DS1825", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 28.243"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS1825"], + "/temperature": [b" 28.243"], + }, }, "42.111111111111": { - ATTR_INJECT_READS: [ - b"DS28EA00", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 29.123"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS28EA00"], + "/temperature": [b" 29.123"], + }, }, "A6.111111111111": { - ATTR_INJECT_READS: [ - b"DS2438", # read device type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 25.123"}, - {ATTR_INJECT_READS: b" 72.7563"}, - {ATTR_INJECT_READS: b" 73.7563"}, - {ATTR_INJECT_READS: b" 74.7563"}, - {ATTR_INJECT_READS: b" 75.7563"}, - { - ATTR_INJECT_READS: ProtocolError, - }, - {ATTR_INJECT_READS: b" 969.265"}, - {ATTR_INJECT_READS: b" 65.8839"}, - {ATTR_INJECT_READS: b" 2.97"}, - {ATTR_INJECT_READS: b" 4.74"}, - {ATTR_INJECT_READS: b" 0.12"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b" 1"}, - ], + ATTR_INJECT_READS: { + "/type": [b"DS2438"], + "/temperature": [b" 25.123"], + "/humidity": [b" 72.7563"], + "/HIH3600/humidity": [b" 73.7563"], + "/HIH4000/humidity": [b" 74.7563"], + "/HIH5030/humidity": [b" 75.7563"], + "/HTM1735/humidity": [ProtocolError], + "/B1-R1-A/pressure": [b" 969.265"], + "/S3-R1-A/illuminance": [b" 65.8839"], + "/VAD": [b" 2.97"], + "/VDD": [b" 4.74"], + "/vis": [b" 0.12"], + "/IAD": [b" 1"], + }, }, "EF.111111111111": { - ATTR_INJECT_READS: [ - b"HobbyBoards_EF", # read type - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 67.745"}, - {ATTR_INJECT_READS: b" 65.541"}, - {ATTR_INJECT_READS: b" 25.123"}, - ], + ATTR_INJECT_READS: { + "/type": [b"HobbyBoards_EF"], + "/humidity/humidity_corrected": [b" 67.745"], + "/humidity/humidity_raw": [b" 65.541"], + "/humidity/temperature": [b" 25.123"], + }, }, "EF.111111111112": { - ATTR_INJECT_READS: [ - b"HB_MOISTURE_METER", # read type - b" 1", # read is_leaf_0 - b" 1", # read is_leaf_1 - b" 0", # read is_leaf_2 - b" 0", # read is_leaf_3 - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 41.745"}, - {ATTR_INJECT_READS: b" 42.541"}, - {ATTR_INJECT_READS: b" 43.123"}, - {ATTR_INJECT_READS: b" 44.123"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"0"}, - {ATTR_INJECT_READS: b"0"}, - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"0"}, - {ATTR_INJECT_READS: b"0"}, - ], + ATTR_INJECT_READS: { + "/type": [b"HB_MOISTURE_METER"], + "/moisture/is_leaf.0": [b" 1"], + "/moisture/is_leaf.1": [b" 1"], + "/moisture/is_leaf.2": [b" 0"], + "/moisture/is_leaf.3": [b" 0"], + "/moisture/sensor.0": [b" 41.745"], + "/moisture/sensor.1": [b" 42.541"], + "/moisture/sensor.2": [b" 43.123"], + "/moisture/sensor.3": [b" 44.123"], + "/moisture/is_moisture.0": [b" 1"], + "/moisture/is_moisture.1": [b" 1"], + "/moisture/is_moisture.2": [b" 0"], + "/moisture/is_moisture.3": [b" 0"], + }, }, "EF.111111111113": { - ATTR_INJECT_READS: [ - b"HB_HUB", # read type - ], - Platform.BINARY_SENSOR: [ - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"0"}, - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"0"}, - ], - Platform.SWITCH: [ - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"0"}, - {ATTR_INJECT_READS: b"1"}, - {ATTR_INJECT_READS: b"0"}, - ], + ATTR_INJECT_READS: { + "/type": [b"HB_HUB"], + "/hub/branch.0": [b" 1"], + "/hub/branch.1": [b" 0"], + "/hub/branch.2": [b" 1"], + "/hub/branch.3": [b" 0"], + "/hub/short.0": [b" 1"], + "/hub/short.1": [b" 0"], + "/hub/short.2": [b" 1"], + "/hub/short.3": [b" 0"], + }, }, "7E.111111111111": { - ATTR_INJECT_READS: [ - b"EDS", # read type - b"EDS0068", # read device_type - note EDS specific - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 13.9375"}, - {ATTR_INJECT_READS: b" 1012.21"}, - {ATTR_INJECT_READS: b" 65.8839"}, - {ATTR_INJECT_READS: b" 41.375"}, - ], + ATTR_INJECT_READS: { + "/type": [b"EDS"], + "/device_type": [b"EDS0068"], + "/EDS0068/temperature": [b" 13.9375"], + "/EDS0068/pressure": [b" 1012.21"], + "/EDS0068/light": [b" 65.8839"], + "/EDS0068/humidity": [b" 41.375"], + }, }, "7E.222222222222": { - ATTR_INJECT_READS: [ - b"EDS", # read type - b"EDS0066", # read device_type - note EDS specific - ], - Platform.SENSOR: [ - {ATTR_INJECT_READS: b" 13.9375"}, - {ATTR_INJECT_READS: b" 1012.21"}, - ], + ATTR_INJECT_READS: { + "/type": [b"EDS"], + "/device_type": [b"EDS0066"], + "/EDS0066/temperature": [b" 13.9375"], + "/EDS0066/pressure": [b" 1012.21"], + }, }, } diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 450cc4c7486..10122ba8685 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -1,1645 +1,789 @@ # serializer version: 1 -# name: test_binary_sensors[00.111111111111] - list([ - ]) -# --- -# name: test_binary_sensors[00.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[00.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[05.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '05.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2405', - 'model_id': None, - 'name': '05.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.12_111111111111_sensed_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_binary_sensors[05.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[05.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[10.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '10.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18S20', - 'model_id': None, - 'name': '10.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.12_111111111111_sensed_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) -# --- -# name: test_binary_sensors[10.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[10.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[12.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '12.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2406', - 'model_id': None, - 'name': '12.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/12.111111111111/sensed.A', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[12.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.12_111111111111_sensed_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/12.111111111111/sensed.A', - 'unit_of_measurement': None, +# name: test_binary_sensors[binary_sensor.12_111111111111_sensed_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/12.111111111111/sensed.A', + 'friendly_name': '12.111111111111 Sensed A', + 'raw_value': 1.0, }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.12_111111111111_sensed_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/12.111111111111/sensed.B', - 'unit_of_measurement': None, + 'context': , + 'entity_id': 'binary_sensor.12_111111111111_sensed_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensors[binary_sensor.12_111111111111_sensed_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_binary_sensors[12.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/12.111111111111/sensed.A', - 'friendly_name': '12.111111111111 Sensed A', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'binary_sensor.12_111111111111_sensed_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.12_111111111111_sensed_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/12.111111111111/sensed.B', - 'friendly_name': '12.111111111111 Sensed B', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.12_111111111111_sensed_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/12.111111111111/sensed.B', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[16.111111111111] - list([ - ]) -# --- -# name: test_binary_sensors[16.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[16.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[1D.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1D.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2423', - 'model_id': None, - 'name': '1D.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.12_111111111111_sensed_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/12.111111111111/sensed.B', + 'friendly_name': '12.111111111111 Sensed B', + 'raw_value': 0.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.12_111111111111_sensed_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[1D.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[1D.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[1F.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1F.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2409', - 'model_id': None, - 'name': '1F.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1D.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2423', - 'model_id': None, - 'name': '1D.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) -# --- -# name: test_binary_sensors[1F.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[1F.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[22.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '22.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS1822', - 'model_id': None, - 'name': '22.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.0', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[22.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[22.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[26.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '26.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2438', - 'model_id': None, - 'name': '26.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.0', + 'friendly_name': '29.111111111111 Sensed 0', + 'raw_value': 1.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) # --- -# name: test_binary_sensors[26.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[26.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[28.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_binary_sensors[28.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[28.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[28.222222222222] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.222222222222', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.222222222222', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.1', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[28.222222222222].1 - list([ - ]) +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.1', + 'friendly_name': '29.111111111111 Sensed 1', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[28.222222222222].2 - list([ - ]) +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.2', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[28.222222222223] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.222222222223', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.222222222223', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.2', + 'friendly_name': '29.111111111111 Sensed 2', + 'raw_value': 0.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[28.222222222223].1 - list([ - ]) +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.3', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[28.222222222223].2 - list([ - ]) +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.3', + 'friendly_name': '29.111111111111 Sensed 3', + 'raw_value': None, + }), + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) # --- -# name: test_binary_sensors[29.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '29.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2408', - 'model_id': None, - 'name': '29.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 4', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.4', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[29.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.0', - 'unit_of_measurement': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.4', + 'friendly_name': '29.111111111111 Sensed 4', + 'raw_value': 0.0, }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.1', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.2', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.3', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 4', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.4', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 5', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.5', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_6', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 6', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.6', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.29_111111111111_sensed_7', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed 7', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/29.111111111111/sensed.7', - 'unit_of_measurement': None, - }), - ]) + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[29.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.0', - 'friendly_name': '29.111111111111 Sensed 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.1', - 'friendly_name': '29.111111111111 Sensed 1', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.2', - 'friendly_name': '29.111111111111 Sensed 2', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.3', - 'friendly_name': '29.111111111111 Sensed 3', - 'raw_value': None, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 5', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.5', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.5', + 'friendly_name': '29.111111111111 Sensed 5', + 'raw_value': 0.0, }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.4', - 'friendly_name': '29.111111111111 Sensed 4', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.5', - 'friendly_name': '29.111111111111 Sensed 5', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.6', - 'friendly_name': '29.111111111111 Sensed 6', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_6', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/sensed.7', - 'friendly_name': '29.111111111111 Sensed 7', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.29_111111111111_sensed_7', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 6', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.6', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.6', + 'friendly_name': '29.111111111111 Sensed 6', + 'raw_value': 0.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[30.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '30.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2760', - 'model_id': None, - 'name': '30.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_binary_sensors[30.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[30.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[3A.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '3A.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2413', - 'model_id': None, - 'name': '3A.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.29_111111111111_sensed_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) -# --- -# name: test_binary_sensors[3A.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.3a_111111111111_sensed_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/3A.111111111111/sensed.A', - 'unit_of_measurement': None, + 'name': None, + 'options': dict({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.3a_111111111111_sensed_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sensed B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sensed_id', - 'unique_id': '/3A.111111111111/sensed.B', - 'unit_of_measurement': None, + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed 7', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/29.111111111111/sensed.7', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[binary_sensor.29_111111111111_sensed_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/sensed.7', + 'friendly_name': '29.111111111111 Sensed 7', + 'raw_value': 0.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.29_111111111111_sensed_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[3A.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/3A.111111111111/sensed.A', - 'friendly_name': '3A.111111111111 Sensed A', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'binary_sensor.3a_111111111111_sensed_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', +# name: test_binary_sensors[binary_sensor.3a_111111111111_sensed_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/3A.111111111111/sensed.B', - 'friendly_name': '3A.111111111111 Sensed B', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.3a_111111111111_sensed_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.3a_111111111111_sensed_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) -# --- -# name: test_binary_sensors[3B.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '3B.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS1825', - 'model_id': None, - 'name': '3B.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/3A.111111111111/sensed.A', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[3B.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[3B.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[42.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '42.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS28EA00', - 'model_id': None, - 'name': '42.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.3a_111111111111_sensed_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/3A.111111111111/sensed.A', + 'friendly_name': '3A.111111111111 Sensed A', + 'raw_value': 1.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.3a_111111111111_sensed_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) # --- -# name: test_binary_sensors[42.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[42.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[7E.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '7E.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Embedded Data Systems', - 'model': 'EDS0068', - 'model_id': None, - 'name': '7E.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.3a_111111111111_sensed_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_binary_sensors[7E.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[7E.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[7E.222222222222] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '7E.222222222222', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Embedded Data Systems', - 'model': 'EDS0066', - 'model_id': None, - 'name': '7E.222222222222', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.3a_111111111111_sensed_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) -# --- -# name: test_binary_sensors[7E.222222222222].1 - list([ - ]) -# --- -# name: test_binary_sensors[7E.222222222222].2 - list([ - ]) -# --- -# name: test_binary_sensors[A6.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'A6.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2438', - 'model_id': None, - 'name': 'A6.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensed B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensed_id', + 'unique_id': '/3A.111111111111/sensed.B', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[A6.111111111111].1 - list([ - ]) -# --- -# name: test_binary_sensors[A6.111111111111].2 - list([ - ]) -# --- -# name: test_binary_sensors[EF.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HobbyBoards_EF', - 'model_id': None, - 'name': 'EF.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.3a_111111111111_sensed_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/3A.111111111111/sensed.B', + 'friendly_name': '3A.111111111111 Sensed B', + 'raw_value': 0.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.3a_111111111111_sensed_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[EF.111111111111].1 - list([ - ]) +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hub short on branch 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_short_id', + 'unique_id': '/EF.111111111113/hub/short.0', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[EF.111111111111].2 - list([ - ]) +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'device_file': '/EF.111111111113/hub/short.0', + 'friendly_name': 'EF.111111111113 Hub short on branch 0', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) # --- -# name: test_binary_sensors[EF.111111111112] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111112', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HB_MOISTURE_METER', - 'model_id': None, - 'name': 'EF.111111111112', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hub short on branch 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_short_id', + 'unique_id': '/EF.111111111113/hub/short.1', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[EF.111111111112].1 - list([ - ]) +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'device_file': '/EF.111111111113/hub/short.1', + 'friendly_name': 'EF.111111111113 Hub short on branch 1', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- -# name: test_binary_sensors[EF.111111111112].2 - list([ - ]) +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hub short on branch 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_short_id', + 'unique_id': '/EF.111111111113/hub/short.2', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[EF.111111111113] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111113', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HB_HUB', - 'model_id': None, - 'name': 'EF.111111111113', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'device_file': '/EF.111111111113/hub/short.2', + 'friendly_name': 'EF.111111111113 Hub short on branch 2', + 'raw_value': 1.0, }), - ]) + 'context': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) # --- -# name: test_binary_sensors[EF.111111111113].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hub short on branch 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_short_id', - 'unique_id': '/EF.111111111113/hub/short.0', - 'unit_of_measurement': None, +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hub short on branch 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_short_id', - 'unique_id': '/EF.111111111113/hub/short.1', - 'unit_of_measurement': None, + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hub short on branch 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_short_id', - 'unique_id': '/EF.111111111113/hub/short.2', - 'unit_of_measurement': None, + 'name': None, + 'options': dict({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hub short on branch 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_short_id', - 'unique_id': '/EF.111111111113/hub/short.3', - 'unit_of_measurement': None, - }), - ]) + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hub short on branch 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_short_id', + 'unique_id': '/EF.111111111113/hub/short.3', + 'unit_of_measurement': None, + }) # --- -# name: test_binary_sensors[EF.111111111113].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'device_file': '/EF.111111111113/hub/short.0', - 'friendly_name': 'EF.111111111113 Hub short on branch 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', +# name: test_binary_sensors[binary_sensor.ef_111111111113_hub_short_on_branch_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'device_file': '/EF.111111111113/hub/short.3', + 'friendly_name': 'EF.111111111113 Hub short on branch 3', + 'raw_value': 0.0, }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'device_file': '/EF.111111111113/hub/short.1', - 'friendly_name': 'EF.111111111113 Hub short on branch 1', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'device_file': '/EF.111111111113/hub/short.2', - 'friendly_name': 'EF.111111111113 Hub short on branch 2', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'device_file': '/EF.111111111113/hub/short.3', - 'friendly_name': 'EF.111111111113 Hub short on branch 3', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) + 'context': , + 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- diff --git a/tests/components/onewire/snapshots/test_diagnostics.ambr b/tests/components/onewire/snapshots/test_diagnostics.ambr index f51fca7e988..c60d0a9748b 100644 --- a/tests/components/onewire/snapshots/test_diagnostics.ambr +++ b/tests/components/onewire/snapshots/test_diagnostics.ambr @@ -12,7 +12,10 @@ ]), 'manufacturer': 'Hobby Boards', 'model': 'HB_HUB', + 'model_id': 'HB_HUB', 'name': 'EF.111111111113', + 'serial_number': '111111111113', + 'sw_version': '3.2', }), 'family': 'EF', 'id': 'EF.111111111113', diff --git a/tests/components/onewire/snapshots/test_init.ambr b/tests/components/onewire/snapshots/test_init.ambr new file mode 100644 index 00000000000..9b2a0e00a62 --- /dev/null +++ b/tests/components/onewire/snapshots/test_init.ambr @@ -0,0 +1,727 @@ +# serializer version: 1 +# name: test_registry[05.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '05.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2405', + 'model_id': 'DS2405', + 'name': '05.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[10.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '10.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS18S20', + 'model_id': 'DS18S20', + 'name': '10.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[12.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '12.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2406', + 'model_id': 'DS2406', + 'name': '12.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[1D.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '1D.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2423', + 'model_id': 'DS2423', + 'name': '1D.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': , + }) +# --- +# name: test_registry[1F.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '1F.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2409', + 'model_id': 'DS2409', + 'name': '1F.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[20.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '20.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2450', + 'model_id': 'DS2450', + 'name': '20.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[22.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '22.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS1822', + 'model_id': 'DS1822', + 'name': '22.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[26.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '26.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2438', + 'model_id': 'DS2438', + 'name': '26.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[28.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '28.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS18B20', + 'model_id': 'DS18B20', + 'name': '28.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[28.222222222222-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '28.222222222222', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS18B20', + 'model_id': 'DS18B20', + 'name': '28.222222222222', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '222222222222', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[28.222222222223-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '28.222222222223', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS18B20', + 'model_id': 'DS18B20', + 'name': '28.222222222223', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '222222222223', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[29.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '29.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2408', + 'model_id': 'DS2408', + 'name': '29.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[30.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '30.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2760', + 'model_id': 'DS2760', + 'name': '30.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[3A.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '3A.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2413', + 'model_id': 'DS2413', + 'name': '3A.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[3B.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '3B.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS1825', + 'model_id': 'DS1825', + 'name': '3B.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[42.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '42.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS28EA00', + 'model_id': 'DS28EA00', + 'name': '42.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[7E.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '7E.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Embedded Data Systems', + 'model': 'EDS0068', + 'model_id': 'EDS0068', + 'name': '7E.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[7E.222222222222-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + '7E.222222222222', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Embedded Data Systems', + 'model': 'EDS0066', + 'model_id': 'EDS0066', + 'name': '7E.222222222222', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '222222222222', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[A6.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'A6.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2438', + 'model_id': 'DS2438', + 'name': 'A6.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[EF.111111111111-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'EF.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Hobby Boards', + 'model': 'HobbyBoards_EF', + 'model_id': 'HobbyBoards_EF', + 'name': 'EF.111111111111', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111111', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[EF.111111111112-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'EF.111111111112', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Hobby Boards', + 'model': 'HB_MOISTURE_METER', + 'model_id': 'HB_MOISTURE_METER', + 'name': 'EF.111111111112', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111112', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- +# name: test_registry[EF.111111111113-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'EF.111111111113', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Hobby Boards', + 'model': 'HB_HUB', + 'model_id': 'HB_HUB', + 'name': 'EF.111111111113', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '111111111113', + 'suggested_area': None, + 'sw_version': '3.2', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/onewire/snapshots/test_select.ambr b/tests/components/onewire/snapshots/test_select.ambr new file mode 100644 index 00000000000..a896d946841 --- /dev/null +++ b/tests/components/onewire/snapshots/test_select.ambr @@ -0,0 +1,63 @@ +# serializer version: 1 +# name: test_selects[select.28_111111111111_temperature_resolution-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '9', + '10', + '11', + '12', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.28_111111111111_temperature_resolution', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature resolution', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tempres', + 'unique_id': '/28.111111111111/tempres', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.28_111111111111_temperature_resolution-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/28.111111111111/tempres', + 'friendly_name': '28.111111111111 Temperature resolution', + 'options': list([ + '9', + '10', + '11', + '12', + ]), + 'raw_value': 12.0, + }), + 'context': , + 'entity_id': 'select.28_111111111111_temperature_resolution', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 261b081060c..eca459b4c57 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -1,3477 +1,3129 @@ # serializer version: 1 -# name: test_sensors[00.111111111111] - list([ - ]) -# --- -# name: test_sensors[00.111111111111].1 - list([ - ]) -# --- -# name: test_sensors[00.111111111111].2 - list([ - ]) -# --- -# name: test_sensors[05.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '05.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2405', - 'model_id': None, - 'name': '05.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_sensors[sensor.10_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_sensors[05.111111111111].1 - list([ - ]) -# --- -# name: test_sensors[05.111111111111].2 - list([ - ]) -# --- -# name: test_sensors[10.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '10.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18S20', - 'model_id': None, - 'name': '10.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - ]) + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.10_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/10.111111111111/temperature', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[10.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.10_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/10.111111111111/temperature', +# name: test_sensors[sensor.10_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/10.111111111111/temperature', + 'friendly_name': '10.111111111111 Temperature', + 'raw_value': 25.123, + 'state_class': , 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.10_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.123', + }) # --- -# name: test_sensors[10.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/10.111111111111/temperature', - 'friendly_name': '10.111111111111 Temperature', - 'raw_value': 25.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.10_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '25.1', +# name: test_sensors[sensor.12_111111111111_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.12_111111111111_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/12.111111111111/TAI8570/pressure', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[12.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '12.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2406', - 'model_id': None, - 'name': '12.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[12.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.12_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/12.111111111111/TAI8570/temperature', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.12_111111111111_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/12.111111111111/TAI8570/pressure', +# name: test_sensors[sensor.12_111111111111_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/12.111111111111/TAI8570/pressure', + 'friendly_name': '12.111111111111 Pressure', + 'raw_value': 1025.123, + 'state_class': , 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.12_111111111111_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1025.123', + }) # --- -# name: test_sensors[12.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/12.111111111111/TAI8570/temperature', - 'friendly_name': '12.111111111111 Temperature', - 'raw_value': 25.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.12_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '25.1', +# name: test_sensors[sensor.12_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/12.111111111111/TAI8570/pressure', - 'friendly_name': '12.111111111111 Pressure', - 'raw_value': 1025.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.12_111111111111_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1025.1', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - ]) + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.12_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/12.111111111111/TAI8570/temperature', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[16.111111111111] - list([ - ]) -# --- -# name: test_sensors[16.111111111111].1 - list([ - ]) -# --- -# name: test_sensors[16.111111111111].2 - list([ - ]) -# --- -# name: test_sensors[1D.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1D.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2423', - 'model_id': None, - 'name': '1D.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[1D.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.1d_111111111111_counter_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Counter A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'counter_id', - 'unique_id': '/1D.111111111111/counter.A', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.1d_111111111111_counter_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Counter B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'counter_id', - 'unique_id': '/1D.111111111111/counter.B', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_sensors[1D.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/1D.111111111111/counter.A', - 'friendly_name': '1D.111111111111 Counter A', - 'raw_value': 251123.0, - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.1d_111111111111_counter_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '251123', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/1D.111111111111/counter.B', - 'friendly_name': '1D.111111111111 Counter B', - 'raw_value': 248125.0, - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.1d_111111111111_counter_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '248125', - }), - ]) -# --- -# name: test_sensors[1F.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1F.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2409', - 'model_id': None, - 'name': '1F.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1D.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2423', - 'model_id': None, - 'name': '1D.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }), - ]) -# --- -# name: test_sensors[1F.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.1d_111111111111_counter_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Counter A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'counter_id', - 'unique_id': '/1D.111111111111/counter.A', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.1d_111111111111_counter_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Counter B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'counter_id', - 'unique_id': '/1D.111111111111/counter.B', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_sensors[1F.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/1F.111111111111/main/1D.111111111111/counter.A', - 'friendly_name': '1D.111111111111 Counter A', - 'raw_value': 251123.0, - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.1d_111111111111_counter_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '251123', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/1F.111111111111/main/1D.111111111111/counter.B', - 'friendly_name': '1D.111111111111 Counter B', - 'raw_value': 248125.0, - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.1d_111111111111_counter_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '248125', - }), - ]) -# --- -# name: test_sensors[22.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '22.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS1822', - 'model_id': None, - 'name': '22.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[22.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.22_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/22.111111111111/temperature', +# name: test_sensors[sensor.12_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/12.111111111111/TAI8570/temperature', + 'friendly_name': '12.111111111111 Temperature', + 'raw_value': 25.123, + 'state_class': , 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.12_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.123', + }) # --- -# name: test_sensors[22.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/22.111111111111/temperature', - 'friendly_name': '22.111111111111 Temperature', - 'raw_value': None, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.22_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', +# name: test_sensors[sensor.1d_111111111111_counter_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - ]) -# --- -# name: test_sensors[26.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '26.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2438', - 'model_id': None, - 'name': '26.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - ]) + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.1d_111111111111_counter_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Counter A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'counter_id', + 'unique_id': '/1D.111111111111/counter.A', + 'unit_of_measurement': None, + }) # --- -# name: test_sensors[26.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/26.111111111111/temperature', +# name: test_sensors[sensor.1d_111111111111_counter_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/1D.111111111111/counter.A', + 'friendly_name': '1D.111111111111 Counter A', + 'raw_value': 251123.0, + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.1d_111111111111_counter_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '251123', + }) +# --- +# name: test_sensors[sensor.1d_111111111111_counter_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.1d_111111111111_counter_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Counter B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'counter_id', + 'unique_id': '/1D.111111111111/counter.B', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.1d_111111111111_counter_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/1D.111111111111/counter.B', + 'friendly_name': '1D.111111111111 Counter B', + 'raw_value': 248125.0, + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.1d_111111111111_counter_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '248125', + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_latest_voltage_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Latest voltage A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latest_voltage_id', + 'unique_id': '/20.111111111111/latestvolt.A', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/latestvolt.A', + 'friendly_name': '20.111111111111 Latest voltage A', + 'raw_value': 1.11, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_latest_voltage_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.11', + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_latest_voltage_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Latest voltage B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latest_voltage_id', + 'unique_id': '/20.111111111111/latestvolt.B', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/latestvolt.B', + 'friendly_name': '20.111111111111 Latest voltage B', + 'raw_value': 2.22, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_latest_voltage_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.22', + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_latest_voltage_c', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Latest voltage C', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latest_voltage_id', + 'unique_id': '/20.111111111111/latestvolt.C', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/latestvolt.C', + 'friendly_name': '20.111111111111 Latest voltage C', + 'raw_value': 3.33, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_latest_voltage_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.33', + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_d-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_latest_voltage_d', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Latest voltage D', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latest_voltage_id', + 'unique_id': '/20.111111111111/latestvolt.D', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_latest_voltage_d-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/latestvolt.D', + 'friendly_name': '20.111111111111 Latest voltage D', + 'raw_value': 4.44, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_latest_voltage_d', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.44', + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_voltage_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_id', + 'unique_id': '/20.111111111111/volt.A', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/volt.A', + 'friendly_name': '20.111111111111 Voltage A', + 'raw_value': 1.1, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_voltage_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.1', + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_voltage_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_id', + 'unique_id': '/20.111111111111/volt.B', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/volt.B', + 'friendly_name': '20.111111111111 Voltage B', + 'raw_value': 2.2, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_voltage_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.2', + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_voltage_c', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage C', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_id', + 'unique_id': '/20.111111111111/volt.C', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/volt.C', + 'friendly_name': '20.111111111111 Voltage C', + 'raw_value': 3.3, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_voltage_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.3', + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_d-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.20_111111111111_voltage_d', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage D', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_id', + 'unique_id': '/20.111111111111/volt.D', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.20_111111111111_voltage_d-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/20.111111111111/volt.D', + 'friendly_name': '20.111111111111 Voltage D', + 'raw_value': 4.4, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.20_111111111111_voltage_d', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.4', + }) +# --- +# name: test_sensors[sensor.22_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.22_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/22.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.22_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/22.111111111111/temperature', + 'friendly_name': '22.111111111111 Temperature', + 'raw_value': None, + 'state_class': , 'unit_of_measurement': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/26.111111111111/humidity', + 'context': , + 'entity_id': 'sensor.22_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.26_111111111111_hih3600_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_hih3600_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH3600 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih3600', + 'unique_id': '/26.111111111111/HIH3600/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.26_111111111111_hih3600_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/26.111111111111/HIH3600/humidity', + 'friendly_name': '26.111111111111 HIH3600 humidity', + 'raw_value': 73.7563, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_hih3600_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HIH3600 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_hih3600', - 'unique_id': '/26.111111111111/HIH3600/humidity', + 'context': , + 'entity_id': 'sensor.26_111111111111_hih3600_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '73.7563', + }) +# --- +# name: test_sensors[sensor.26_111111111111_hih4000_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_hih4000_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH4000 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih4000', + 'unique_id': '/26.111111111111/HIH4000/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.26_111111111111_hih4000_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/26.111111111111/HIH4000/humidity', + 'friendly_name': '26.111111111111 HIH4000 humidity', + 'raw_value': 74.7563, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_hih4000_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HIH4000 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_hih4000', - 'unique_id': '/26.111111111111/HIH4000/humidity', + 'context': , + 'entity_id': 'sensor.26_111111111111_hih4000_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '74.7563', + }) +# --- +# name: test_sensors[sensor.26_111111111111_hih5030_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_hih5030_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH5030 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih5030', + 'unique_id': '/26.111111111111/HIH5030/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.26_111111111111_hih5030_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/26.111111111111/HIH5030/humidity', + 'friendly_name': '26.111111111111 HIH5030 humidity', + 'raw_value': 75.7563, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_hih5030_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HIH5030 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_hih5030', - 'unique_id': '/26.111111111111/HIH5030/humidity', + 'context': , + 'entity_id': 'sensor.26_111111111111_hih5030_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.7563', + }) +# --- +# name: test_sensors[sensor.26_111111111111_htm1735_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_htm1735_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HTM1735 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_htm1735', + 'unique_id': '/26.111111111111/HTM1735/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.26_111111111111_htm1735_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/26.111111111111/HTM1735/humidity', + 'friendly_name': '26.111111111111 HTM1735 humidity', + 'raw_value': None, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_htm1735_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HTM1735 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_htm1735', - 'unique_id': '/26.111111111111/HTM1735/humidity', + 'context': , + 'entity_id': 'sensor.26_111111111111_htm1735_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.26_111111111111_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/26.111111111111/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.26_111111111111_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/26.111111111111/humidity', + 'friendly_name': '26.111111111111 Humidity', + 'raw_value': 72.7563, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/26.111111111111/B1-R1-A/pressure', - 'unit_of_measurement': , + 'context': , + 'entity_id': 'sensor.26_111111111111_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '72.7563', + }) +# --- +# name: test_sensors[sensor.26_111111111111_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_illuminance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Illuminance', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/26.111111111111/S3-R1-A/illuminance', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/26.111111111111/S3-R1-A/illuminance', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensors[sensor.26_111111111111_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'device_file': '/26.111111111111/S3-R1-A/illuminance', + 'friendly_name': '26.111111111111 Illuminance', + 'raw_value': 65.8839, + 'state_class': , 'unit_of_measurement': 'lx', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_vad_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VAD voltage', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vad', - 'unique_id': '/26.111111111111/VAD', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_vdd_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VDD voltage', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vdd', - 'unique_id': '/26.111111111111/VDD', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.26_111111111111_vis_voltage_difference', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VIS voltage difference', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vis', - 'unique_id': '/26.111111111111/vis', - 'unit_of_measurement': , - }), - ]) + 'context': , + 'entity_id': 'sensor.26_111111111111_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65.8839', + }) # --- -# name: test_sensors[26.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/26.111111111111/temperature', - 'friendly_name': '26.111111111111 Temperature', - 'raw_value': 25.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '25.1', +# name: test_sensors[sensor.26_111111111111_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/26.111111111111/humidity', - 'friendly_name': '26.111111111111 Humidity', - 'raw_value': 72.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '72.8', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/26.111111111111/HIH3600/humidity', - 'friendly_name': '26.111111111111 HIH3600 humidity', - 'raw_value': 73.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_hih3600_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '73.8', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/26.111111111111/HIH4000/humidity', - 'friendly_name': '26.111111111111 HIH4000 humidity', - 'raw_value': 74.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_hih4000_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '74.8', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/26.111111111111/HIH5030/humidity', - 'friendly_name': '26.111111111111 HIH5030 humidity', - 'raw_value': 75.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_hih5030_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '75.8', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/26.111111111111/HTM1735/humidity', - 'friendly_name': '26.111111111111 HTM1735 humidity', - 'raw_value': None, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_htm1735_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/26.111111111111/B1-R1-A/pressure', - 'friendly_name': '26.111111111111 Pressure', - 'raw_value': 969.265, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '969.3', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'illuminance', - 'device_file': '/26.111111111111/S3-R1-A/illuminance', - 'friendly_name': '26.111111111111 Illuminance', - 'raw_value': 65.8839, - 'state_class': , - 'unit_of_measurement': 'lx', - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_illuminance', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '65.9', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/26.111111111111/VAD', - 'friendly_name': '26.111111111111 VAD voltage', - 'raw_value': 2.97, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_vad_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.0', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/26.111111111111/VDD', - 'friendly_name': '26.111111111111 VDD voltage', - 'raw_value': 4.74, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_vdd_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4.7', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/26.111111111111/vis', - 'friendly_name': '26.111111111111 VIS voltage difference', - 'raw_value': 0.12, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.26_111111111111_vis_voltage_difference', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.1', - }), - ]) + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/26.111111111111/B1-R1-A/pressure', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[28.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[28.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.28_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/28.111111111111/temperature', - 'unit_of_measurement': , - }), - ]) -# --- -# name: test_sensors[28.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/28.111111111111/temperature', - 'friendly_name': '28.111111111111 Temperature', - 'raw_value': 26.984, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.28_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '27.0', - }), - ]) -# --- -# name: test_sensors[28.222222222222] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.222222222222', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.222222222222', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[28.222222222222].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.28_222222222222_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/28.222222222222/temperature', - 'unit_of_measurement': , - }), - ]) -# --- -# name: test_sensors[28.222222222222].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/28.222222222222/temperature9', - 'friendly_name': '28.222222222222 Temperature', - 'raw_value': 26.984, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.28_222222222222_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '27.0', - }), - ]) -# --- -# name: test_sensors[28.222222222223] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.222222222223', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.222222222223', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[28.222222222223].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.28_222222222223_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/28.222222222223/temperature', - 'unit_of_measurement': , - }), - ]) -# --- -# name: test_sensors[28.222222222223].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/28.222222222223/temperature', - 'friendly_name': '28.222222222223 Temperature', - 'raw_value': 26.984, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.28_222222222223_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '27.0', - }), - ]) -# --- -# name: test_sensors[29.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '29.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2408', - 'model_id': None, - 'name': '29.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[29.111111111111].1 - list([ - ]) -# --- -# name: test_sensors[29.111111111111].2 - list([ - ]) -# --- -# name: test_sensors[30.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '30.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2760', - 'model_id': None, - 'name': '30.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[30.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.30_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/30.111111111111/temperature', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.30_111111111111_thermocouple_k_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Thermocouple K temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'thermocouple_temperature_k', - 'unique_id': '/30.111111111111/typeX/temperature', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.30_111111111111_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Voltage', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/30.111111111111/volt', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.30_111111111111_vis_voltage_gradient', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VIS voltage gradient', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vis_gradient', - 'unique_id': '/30.111111111111/vis', - 'unit_of_measurement': , - }), - ]) -# --- -# name: test_sensors[30.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/30.111111111111/temperature', - 'friendly_name': '30.111111111111 Temperature', - 'raw_value': 26.984, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.30_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '27.0', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/30.111111111111/typeK/temperature', - 'friendly_name': '30.111111111111 Thermocouple K temperature', - 'raw_value': 173.7563, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.30_111111111111_thermocouple_k_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '173.8', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/30.111111111111/volt', - 'friendly_name': '30.111111111111 Voltage', - 'raw_value': 2.97, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.30_111111111111_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.0', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/30.111111111111/vis', - 'friendly_name': '30.111111111111 VIS voltage gradient', - 'raw_value': 0.12, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.30_111111111111_vis_voltage_gradient', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.1', - }), - ]) -# --- -# name: test_sensors[3A.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '3A.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2413', - 'model_id': None, - 'name': '3A.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[3A.111111111111].1 - list([ - ]) -# --- -# name: test_sensors[3A.111111111111].2 - list([ - ]) -# --- -# name: test_sensors[3B.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '3B.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS1825', - 'model_id': None, - 'name': '3B.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[3B.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.3b_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/3B.111111111111/temperature', - 'unit_of_measurement': , - }), - ]) -# --- -# name: test_sensors[3B.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/3B.111111111111/temperature', - 'friendly_name': '3B.111111111111 Temperature', - 'raw_value': 28.243, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.3b_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '28.2', - }), - ]) -# --- -# name: test_sensors[42.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '42.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS28EA00', - 'model_id': None, - 'name': '42.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[42.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.42_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/42.111111111111/temperature', - 'unit_of_measurement': , - }), - ]) -# --- -# name: test_sensors[42.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/42.111111111111/temperature', - 'friendly_name': '42.111111111111 Temperature', - 'raw_value': 29.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.42_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '29.1', - }), - ]) -# --- -# name: test_sensors[7E.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '7E.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Embedded Data Systems', - 'model': 'EDS0068', - 'model_id': None, - 'name': '7E.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[7E.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.7e_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/7E.111111111111/EDS0068/temperature', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.7e_111111111111_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/7E.111111111111/EDS0068/pressure', +# name: test_sensors[sensor.26_111111111111_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/26.111111111111/B1-R1-A/pressure', + 'friendly_name': '26.111111111111 Pressure', + 'raw_value': 969.265, + 'state_class': , 'unit_of_measurement': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.7e_111111111111_illuminance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Illuminance', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/7E.111111111111/EDS0068/light', + 'context': , + 'entity_id': 'sensor.26_111111111111_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '969.265', + }) +# --- +# name: test_sensors[sensor.26_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/26.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.26_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/26.111111111111/temperature', + 'friendly_name': '26.111111111111 Temperature', + 'raw_value': 25.123, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.26_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.123', + }) +# --- +# name: test_sensors[sensor.26_111111111111_vad_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_vad_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VAD voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vad', + 'unique_id': '/26.111111111111/VAD', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.26_111111111111_vad_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/26.111111111111/VAD', + 'friendly_name': '26.111111111111 VAD voltage', + 'raw_value': 2.97, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.26_111111111111_vad_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.97', + }) +# --- +# name: test_sensors[sensor.26_111111111111_vdd_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_vdd_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VDD voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vdd', + 'unique_id': '/26.111111111111/VDD', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.26_111111111111_vdd_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/26.111111111111/VDD', + 'friendly_name': '26.111111111111 VDD voltage', + 'raw_value': 4.74, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.26_111111111111_vdd_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.74', + }) +# --- +# name: test_sensors[sensor.26_111111111111_vis_voltage_difference-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.26_111111111111_vis_voltage_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VIS voltage difference', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vis', + 'unique_id': '/26.111111111111/vis', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.26_111111111111_vis_voltage_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/26.111111111111/vis', + 'friendly_name': '26.111111111111 VIS voltage difference', + 'raw_value': 0.12, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.26_111111111111_vis_voltage_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.12', + }) +# --- +# name: test_sensors[sensor.28_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.28_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/28.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.28_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/28.111111111111/temperature', + 'friendly_name': '28.111111111111 Temperature', + 'raw_value': 26.984, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.28_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.984', + }) +# --- +# name: test_sensors[sensor.28_222222222222_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.28_222222222222_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/28.222222222222/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.28_222222222222_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/28.222222222222/temperature9', + 'friendly_name': '28.222222222222 Temperature', + 'raw_value': 26.984, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.28_222222222222_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.984', + }) +# --- +# name: test_sensors[sensor.28_222222222223_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.28_222222222223_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/28.222222222223/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.28_222222222223_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/28.222222222223/temperature', + 'friendly_name': '28.222222222223 Temperature', + 'raw_value': 26.984, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.28_222222222223_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.984', + }) +# --- +# name: test_sensors[sensor.30_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.30_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/30.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.30_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/30.111111111111/temperature', + 'friendly_name': '30.111111111111 Temperature', + 'raw_value': 26.984, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.30_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '26.984', + }) +# --- +# name: test_sensors[sensor.30_111111111111_thermocouple_k_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.30_111111111111_thermocouple_k_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Thermocouple K temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thermocouple_temperature_k', + 'unique_id': '/30.111111111111/typeX/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.30_111111111111_thermocouple_k_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/30.111111111111/typeK/temperature', + 'friendly_name': '30.111111111111 Thermocouple K temperature', + 'raw_value': 173.7563, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.30_111111111111_thermocouple_k_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '173.7563', + }) +# --- +# name: test_sensors[sensor.30_111111111111_vis_voltage_gradient-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.30_111111111111_vis_voltage_gradient', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VIS voltage gradient', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vis_gradient', + 'unique_id': '/30.111111111111/vis', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.30_111111111111_vis_voltage_gradient-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/30.111111111111/vis', + 'friendly_name': '30.111111111111 VIS voltage gradient', + 'raw_value': 0.12, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.30_111111111111_vis_voltage_gradient', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.12', + }) +# --- +# name: test_sensors[sensor.30_111111111111_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.30_111111111111_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/30.111111111111/volt', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.30_111111111111_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/30.111111111111/volt', + 'friendly_name': '30.111111111111 Voltage', + 'raw_value': 2.97, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.30_111111111111_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.97', + }) +# --- +# name: test_sensors[sensor.3b_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.3b_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/3B.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.3b_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/3B.111111111111/temperature', + 'friendly_name': '3B.111111111111 Temperature', + 'raw_value': 28.243, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.3b_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.243', + }) +# --- +# name: test_sensors[sensor.42_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.42_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/42.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.42_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/42.111111111111/temperature', + 'friendly_name': '42.111111111111 Temperature', + 'raw_value': 29.123, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.42_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.123', + }) +# --- +# name: test_sensors[sensor.7e_111111111111_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.7e_111111111111_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/7E.111111111111/EDS0068/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.7e_111111111111_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/7E.111111111111/EDS0068/humidity', + 'friendly_name': '7E.111111111111 Humidity', + 'raw_value': 41.375, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.7e_111111111111_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '41.375', + }) +# --- +# name: test_sensors[sensor.7e_111111111111_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.7e_111111111111_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/7E.111111111111/EDS0068/light', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensors[sensor.7e_111111111111_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'device_file': '/7E.111111111111/EDS0068/light', + 'friendly_name': '7E.111111111111 Illuminance', + 'raw_value': 65.8839, + 'state_class': , 'unit_of_measurement': 'lx', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.7e_111111111111_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/7E.111111111111/EDS0068/humidity', - 'unit_of_measurement': '%', - }), - ]) + 'context': , + 'entity_id': 'sensor.7e_111111111111_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65.8839', + }) # --- -# name: test_sensors[7E.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/7E.111111111111/EDS0068/temperature', - 'friendly_name': '7E.111111111111 Temperature', - 'raw_value': 13.9375, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.7e_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '13.9', +# name: test_sensors[sensor.7e_111111111111_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/7E.111111111111/EDS0068/pressure', - 'friendly_name': '7E.111111111111 Pressure', - 'raw_value': 1012.21, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.7e_111111111111_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1012.2', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'illuminance', - 'device_file': '/7E.111111111111/EDS0068/light', - 'friendly_name': '7E.111111111111 Illuminance', - 'raw_value': 65.8839, - 'state_class': , - 'unit_of_measurement': 'lx', - }), - 'context': , - 'entity_id': 'sensor.7e_111111111111_illuminance', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '65.9', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.7e_111111111111_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/7E.111111111111/EDS0068/humidity', - 'friendly_name': '7E.111111111111 Humidity', - 'raw_value': 41.375, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.7e_111111111111_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '41.4', + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/7E.111111111111/EDS0068/pressure', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[7E.222222222222] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '7E.222222222222', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Embedded Data Systems', - 'model': 'EDS0066', - 'model_id': None, - 'name': '7E.222222222222', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[7E.222222222222].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.7e_222222222222_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/7E.222222222222/EDS0066/temperature', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.7e_222222222222_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/7E.222222222222/EDS0066/pressure', +# name: test_sensors[sensor.7e_111111111111_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/7E.111111111111/EDS0068/pressure', + 'friendly_name': '7E.111111111111 Pressure', + 'raw_value': 1012.21, + 'state_class': , 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.7e_111111111111_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1012.21', + }) # --- -# name: test_sensors[7E.222222222222].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/7E.222222222222/EDS0066/temperature', - 'friendly_name': '7E.222222222222 Temperature', - 'raw_value': 13.9375, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.7e_222222222222_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '13.9', +# name: test_sensors[sensor.7e_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/7E.222222222222/EDS0066/pressure', - 'friendly_name': '7E.222222222222 Pressure', - 'raw_value': 1012.21, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.7e_222222222222_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1012.2', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - ]) + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.7e_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/7E.111111111111/EDS0068/temperature', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[A6.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'A6.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2438', - 'model_id': None, - 'name': 'A6.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_sensors[A6.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/A6.111111111111/temperature', +# name: test_sensors[sensor.7e_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/7E.111111111111/EDS0068/temperature', + 'friendly_name': '7E.111111111111 Temperature', + 'raw_value': 13.9375, + 'state_class': , 'unit_of_measurement': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/A6.111111111111/humidity', - 'unit_of_measurement': '%', + 'context': , + 'entity_id': 'sensor.7e_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.9375', + }) +# --- +# name: test_sensors[sensor.7e_222222222222_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_hih3600_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HIH3600 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_hih3600', - 'unique_id': '/A6.111111111111/HIH3600/humidity', - 'unit_of_measurement': '%', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_hih4000_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HIH4000 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_hih4000', - 'unique_id': '/A6.111111111111/HIH4000/humidity', - 'unit_of_measurement': '%', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.7e_222222222222_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_hih5030_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HIH5030 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_hih5030', - 'unique_id': '/A6.111111111111/HIH5030/humidity', - 'unit_of_measurement': '%', + 'name': None, + 'options': dict({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_htm1735_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'HTM1735 humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_htm1735', - 'unique_id': '/A6.111111111111/HTM1735/humidity', - 'unit_of_measurement': '%', - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_pressure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Pressure', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/A6.111111111111/B1-R1-A/pressure', + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/7E.222222222222/EDS0066/pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.7e_222222222222_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/7E.222222222222/EDS0066/pressure', + 'friendly_name': '7E.222222222222 Pressure', + 'raw_value': 1012.21, + 'state_class': , 'unit_of_measurement': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_illuminance', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Illuminance', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/A6.111111111111/S3-R1-A/illuminance', + 'context': , + 'entity_id': 'sensor.7e_222222222222_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1012.21', + }) +# --- +# name: test_sensors[sensor.7e_222222222222_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.7e_222222222222_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/7E.222222222222/EDS0066/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.7e_222222222222_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/7E.222222222222/EDS0066/temperature', + 'friendly_name': '7E.222222222222 Temperature', + 'raw_value': 13.9375, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.7e_222222222222_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.9375', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_hih3600_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_hih3600_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH3600 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih3600', + 'unique_id': '/A6.111111111111/HIH3600/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_hih3600_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HIH3600/humidity', + 'friendly_name': 'A6.111111111111 HIH3600 humidity', + 'raw_value': 73.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_hih3600_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '73.7563', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_hih4000_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_hih4000_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH4000 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih4000', + 'unique_id': '/A6.111111111111/HIH4000/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_hih4000_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HIH4000/humidity', + 'friendly_name': 'A6.111111111111 HIH4000 humidity', + 'raw_value': 74.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_hih4000_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '74.7563', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_hih5030_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_hih5030_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH5030 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih5030', + 'unique_id': '/A6.111111111111/HIH5030/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_hih5030_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HIH5030/humidity', + 'friendly_name': 'A6.111111111111 HIH5030 humidity', + 'raw_value': 75.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_hih5030_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.7563', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_htm1735_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_htm1735_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HTM1735 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_htm1735', + 'unique_id': '/A6.111111111111/HTM1735/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_htm1735_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HTM1735/humidity', + 'friendly_name': 'A6.111111111111 HTM1735 humidity', + 'raw_value': None, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_htm1735_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/humidity', + 'friendly_name': 'A6.111111111111 Humidity', + 'raw_value': 72.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '72.7563', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_illuminance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/S3-R1-A/illuminance', + 'unit_of_measurement': 'lx', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_illuminance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'device_file': '/A6.111111111111/S3-R1-A/illuminance', + 'friendly_name': 'A6.111111111111 Illuminance', + 'raw_value': 65.8839, + 'state_class': , 'unit_of_measurement': 'lx', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_vad_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VAD voltage', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vad', - 'unique_id': '/A6.111111111111/VAD', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_vdd_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VDD voltage', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vdd', - 'unique_id': '/A6.111111111111/VDD', - 'unit_of_measurement': , - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.a6_111111111111_vis_voltage_difference', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'VIS voltage difference', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'voltage_vis', - 'unique_id': '/A6.111111111111/vis', - 'unit_of_measurement': , - }), - ]) + 'context': , + 'entity_id': 'sensor.a6_111111111111_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65.8839', + }) # --- -# name: test_sensors[A6.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/A6.111111111111/temperature', - 'friendly_name': 'A6.111111111111 Temperature', - 'raw_value': 25.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '25.1', +# name: test_sensors[sensor.a6_111111111111_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/A6.111111111111/humidity', - 'friendly_name': 'A6.111111111111 Humidity', - 'raw_value': 72.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '72.8', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/A6.111111111111/HIH3600/humidity', - 'friendly_name': 'A6.111111111111 HIH3600 humidity', - 'raw_value': 73.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_hih3600_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '73.8', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/A6.111111111111/HIH4000/humidity', - 'friendly_name': 'A6.111111111111 HIH4000 humidity', - 'raw_value': 74.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_hih4000_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '74.8', + 'name': None, + 'options': dict({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/A6.111111111111/HIH5030/humidity', - 'friendly_name': 'A6.111111111111 HIH5030 humidity', - 'raw_value': 75.7563, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_hih5030_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '75.8', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/A6.111111111111/HTM1735/humidity', - 'friendly_name': 'A6.111111111111 HTM1735 humidity', - 'raw_value': None, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_htm1735_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/A6.111111111111/B1-R1-A/pressure', - 'friendly_name': 'A6.111111111111 Pressure', - 'raw_value': 969.265, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_pressure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '969.3', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'illuminance', - 'device_file': '/A6.111111111111/S3-R1-A/illuminance', - 'friendly_name': 'A6.111111111111 Illuminance', - 'raw_value': 65.8839, - 'state_class': , - 'unit_of_measurement': 'lx', - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_illuminance', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '65.9', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/A6.111111111111/VAD', - 'friendly_name': 'A6.111111111111 VAD voltage', - 'raw_value': 2.97, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_vad_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.0', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/A6.111111111111/VDD', - 'friendly_name': 'A6.111111111111 VDD voltage', - 'raw_value': 4.74, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_vdd_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4.7', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'device_file': '/A6.111111111111/vis', - 'friendly_name': 'A6.111111111111 VIS voltage difference', - 'raw_value': 0.12, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.a6_111111111111_vis_voltage_difference', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.1', - }), - ]) + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/B1-R1-A/pressure', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[EF.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HobbyBoards_EF', - 'model_id': None, - 'name': 'EF.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_sensors[sensor.a6_111111111111_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/A6.111111111111/B1-R1-A/pressure', + 'friendly_name': 'A6.111111111111 Pressure', + 'raw_value': 969.265, + 'state_class': , + 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.a6_111111111111_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '969.265', + }) # --- -# name: test_sensors[EF.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111111_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/EF.111111111111/humidity/humidity_corrected', - 'unit_of_measurement': '%', +# name: test_sensors[sensor.a6_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111111_raw_humidity', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Raw humidity', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'humidity_raw', - 'unique_id': '/EF.111111111111/humidity/humidity_raw', - 'unit_of_measurement': '%', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111111_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '/EF.111111111111/humidity/temperature', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.a6_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/A6.111111111111/temperature', + 'friendly_name': 'A6.111111111111 Temperature', + 'raw_value': 25.123, + 'state_class': , 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.a6_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.123', + }) # --- -# name: test_sensors[EF.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/EF.111111111111/humidity/humidity_corrected', - 'friendly_name': 'EF.111111111111 Humidity', - 'raw_value': 67.745, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ef_111111111111_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '67.7', +# name: test_sensors[sensor.a6_111111111111_vad_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/EF.111111111111/humidity/humidity_raw', - 'friendly_name': 'EF.111111111111 Raw humidity', - 'raw_value': 65.541, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ef_111111111111_raw_humidity', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '65.5', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'device_file': '/EF.111111111111/humidity/temperature', - 'friendly_name': 'EF.111111111111 Temperature', - 'raw_value': 25.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ef_111111111111_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '25.1', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_vad_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - ]) + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VAD voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vad', + 'unique_id': '/A6.111111111111/VAD', + 'unit_of_measurement': , + }) # --- -# name: test_sensors[EF.111111111112] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111112', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HB_MOISTURE_METER', - 'model_id': None, - 'name': 'EF.111111111112', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_sensors[sensor.a6_111111111111_vad_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/A6.111111111111/VAD', + 'friendly_name': 'A6.111111111111 VAD voltage', + 'raw_value': 2.97, + 'state_class': , + 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.a6_111111111111_vad_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.97', + }) # --- -# name: test_sensors[EF.111111111112].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111112_wetness_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wetness 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wetness_id', - 'unique_id': '/EF.111111111112/moisture/sensor.0', +# name: test_sensors[sensor.a6_111111111111_vdd_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_vdd_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VDD voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vdd', + 'unique_id': '/A6.111111111111/VDD', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.a6_111111111111_vdd_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/A6.111111111111/VDD', + 'friendly_name': 'A6.111111111111 VDD voltage', + 'raw_value': 4.74, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_vdd_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.74', + }) +# --- +# name: test_sensors[sensor.a6_111111111111_vis_voltage_difference-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_vis_voltage_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VIS voltage difference', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vis', + 'unique_id': '/A6.111111111111/vis', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.a6_111111111111_vis_voltage_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/A6.111111111111/vis', + 'friendly_name': 'A6.111111111111 VIS voltage difference', + 'raw_value': 0.12, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_vis_voltage_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.12', + }) +# --- +# name: test_sensors[sensor.ef_111111111111_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111111_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/EF.111111111111/humidity/humidity_corrected', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.ef_111111111111_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/EF.111111111111/humidity/humidity_corrected', + 'friendly_name': 'EF.111111111111 Humidity', + 'raw_value': 67.745, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111112_wetness_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Wetness 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wetness_id', - 'unique_id': '/EF.111111111112/moisture/sensor.1', + 'context': , + 'entity_id': 'sensor.ef_111111111111_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '67.745', + }) +# --- +# name: test_sensors[sensor.ef_111111111111_raw_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111111_raw_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Raw humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_raw', + 'unique_id': '/EF.111111111111/humidity/humidity_raw', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.ef_111111111111_raw_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/EF.111111111111/humidity/humidity_raw', + 'friendly_name': 'EF.111111111111 Raw humidity', + 'raw_value': 65.541, + 'state_class': , 'unit_of_measurement': '%', }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111112_moisture_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Moisture 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'moisture_id', - 'unique_id': '/EF.111111111112/moisture/sensor.2', + 'context': , + 'entity_id': 'sensor.ef_111111111111_raw_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65.541', + }) +# --- +# name: test_sensors[sensor.ef_111111111111_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/EF.111111111111/humidity/temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.ef_111111111111_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/EF.111111111111/humidity/temperature', + 'friendly_name': 'EF.111111111111 Temperature', + 'raw_value': 25.123, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ef_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.123', + }) +# --- +# name: test_sensors[sensor.ef_111111111112_moisture_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111112_moisture_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_id', + 'unique_id': '/EF.111111111112/moisture/sensor.2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.ef_111111111112_moisture_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/EF.111111111112/moisture/sensor.2', + 'friendly_name': 'EF.111111111112 Moisture 2', + 'raw_value': 43.123, + 'state_class': , 'unit_of_measurement': , }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.ef_111111111112_moisture_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Moisture 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'moisture_id', - 'unique_id': '/EF.111111111112/moisture/sensor.3', + 'context': , + 'entity_id': 'sensor.ef_111111111112_moisture_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '43.123', + }) +# --- +# name: test_sensors[sensor.ef_111111111112_moisture_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111112_moisture_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_id', + 'unique_id': '/EF.111111111112/moisture/sensor.3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.ef_111111111112_moisture_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/EF.111111111112/moisture/sensor.3', + 'friendly_name': 'EF.111111111112 Moisture 3', + 'raw_value': 44.123, + 'state_class': , 'unit_of_measurement': , }), - ]) + 'context': , + 'entity_id': 'sensor.ef_111111111112_moisture_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '44.123', + }) # --- -# name: test_sensors[EF.111111111112].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/EF.111111111112/moisture/sensor.0', - 'friendly_name': 'EF.111111111112 Wetness 0', - 'raw_value': 41.745, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ef_111111111112_wetness_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '41.7', +# name: test_sensors[sensor.ef_111111111112_wetness_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'humidity', - 'device_file': '/EF.111111111112/moisture/sensor.1', - 'friendly_name': 'EF.111111111112 Wetness 1', - 'raw_value': 42.541, - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ef_111111111112_wetness_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '42.5', + 'area_id': None, + 'capabilities': dict({ + 'state_class': , }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/EF.111111111112/moisture/sensor.2', - 'friendly_name': 'EF.111111111112 Moisture 2', - 'raw_value': 43.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ef_111111111112_moisture_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '43.1', + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111112_wetness_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'pressure', - 'device_file': '/EF.111111111112/moisture/sensor.3', - 'friendly_name': 'EF.111111111112 Moisture 3', - 'raw_value': 44.123, - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ef_111111111112_moisture_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '44.1', + 'name': None, + 'options': dict({ }), - ]) + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wetness 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wetness_id', + 'unique_id': '/EF.111111111112/moisture/sensor.0', + 'unit_of_measurement': '%', + }) # --- -# name: test_sensors[EF.111111111113] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111113', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HB_HUB', - 'model_id': None, - 'name': 'EF.111111111113', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, +# name: test_sensors[sensor.ef_111111111112_wetness_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/EF.111111111112/moisture/sensor.0', + 'friendly_name': 'EF.111111111112 Wetness 0', + 'raw_value': 41.745, + 'state_class': , + 'unit_of_measurement': '%', }), - ]) + 'context': , + 'entity_id': 'sensor.ef_111111111112_wetness_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '41.745', + }) # --- -# name: test_sensors[EF.111111111113].1 - list([ - ]) +# name: test_sensors[sensor.ef_111111111112_wetness_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.ef_111111111112_wetness_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wetness 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wetness_id', + 'unique_id': '/EF.111111111112/moisture/sensor.1', + 'unit_of_measurement': '%', + }) # --- -# name: test_sensors[EF.111111111113].2 - list([ - ]) +# name: test_sensors[sensor.ef_111111111112_wetness_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/EF.111111111112/moisture/sensor.1', + 'friendly_name': 'EF.111111111112 Wetness 1', + 'raw_value': 42.541, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ef_111111111112_wetness_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.541', + }) # --- diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 3bc7a2d3def..8be414c7c1e 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -1,2565 +1,1814 @@ # serializer version: 1 -# name: test_switches[00.111111111111] - list([ - ]) -# --- -# name: test_switches[00.111111111111].1 - list([ - ]) -# --- -# name: test_switches[00.111111111111].2 - list([ - ]) -# --- -# name: test_switches[05.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '05.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2405', - 'model_id': None, - 'name': '05.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[05.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.05_111111111111_programmed_input_output', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio', - 'unique_id': '/05.111111111111/PIO', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[05.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/05.111111111111/PIO', - 'friendly_name': '05.111111111111 Programmed input-output', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.05_111111111111_programmed_input_output', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - ]) -# --- -# name: test_switches[10.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '10.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18S20', - 'model_id': None, - 'name': '10.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[10.111111111111].1 - list([ - ]) -# --- -# name: test_switches[10.111111111111].2 - list([ - ]) -# --- -# name: test_switches[12.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '12.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2406', - 'model_id': None, - 'name': '12.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[12.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.12_111111111111_programmed_input_output_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/12.111111111111/PIO.A', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.12_111111111111_programmed_input_output_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/12.111111111111/PIO.B', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.12_111111111111_latch_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/12.111111111111/latch.A', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.12_111111111111_latch_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/12.111111111111/latch.B', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[12.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/12.111111111111/PIO.A', - 'friendly_name': '12.111111111111 Programmed input-output A', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.12_111111111111_programmed_input_output_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/12.111111111111/PIO.B', - 'friendly_name': '12.111111111111 Programmed input-output B', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.12_111111111111_programmed_input_output_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/12.111111111111/latch.A', - 'friendly_name': '12.111111111111 Latch A', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.12_111111111111_latch_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/12.111111111111/latch.B', - 'friendly_name': '12.111111111111 Latch B', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.12_111111111111_latch_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) -# --- -# name: test_switches[16.111111111111] - list([ - ]) -# --- -# name: test_switches[16.111111111111].1 - list([ - ]) -# --- -# name: test_switches[16.111111111111].2 - list([ - ]) -# --- -# name: test_switches[1D.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1D.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2423', - 'model_id': None, - 'name': '1D.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[1D.111111111111].1 - list([ - ]) -# --- -# name: test_switches[1D.111111111111].2 - list([ - ]) -# --- -# name: test_switches[1F.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1F.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2409', - 'model_id': None, - 'name': '1F.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '1D.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2423', - 'model_id': None, - 'name': '1D.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }), - ]) -# --- -# name: test_switches[1F.111111111111].1 - list([ - ]) -# --- -# name: test_switches[1F.111111111111].2 - list([ - ]) -# --- -# name: test_switches[22.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '22.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS1822', - 'model_id': None, - 'name': '22.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[22.111111111111].1 - list([ - ]) -# --- -# name: test_switches[22.111111111111].2 - list([ - ]) -# --- -# name: test_switches[26.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '26.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2438', - 'model_id': None, - 'name': '26.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[26.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.26_111111111111_current_a_d_control', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Current A/D control', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'iad', - 'unique_id': '/26.111111111111/IAD', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[26.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/26.111111111111/IAD', - 'friendly_name': '26.111111111111 Current A/D control', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.26_111111111111_current_a_d_control', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - ]) -# --- -# name: test_switches[28.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[28.111111111111].1 - list([ - ]) -# --- -# name: test_switches[28.111111111111].2 - list([ - ]) -# --- -# name: test_switches[28.222222222222] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.222222222222', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.222222222222', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[28.222222222222].1 - list([ - ]) -# --- -# name: test_switches[28.222222222222].2 - list([ - ]) -# --- -# name: test_switches[28.222222222223] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '28.222222222223', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS18B20', - 'model_id': None, - 'name': '28.222222222223', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[28.222222222223].1 - list([ - ]) -# --- -# name: test_switches[28.222222222223].2 - list([ - ]) -# --- -# name: test_switches[29.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '29.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2408', - 'model_id': None, - 'name': '29.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[29.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.0', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.1', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.2', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.3', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 4', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.4', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 5', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.5', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_6', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 6', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.6', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_programmed_input_output_7', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output 7', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/29.111111111111/PIO.7', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.0', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.1', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.2', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.3', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 4', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.4', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_5', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 5', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.5', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_6', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 6', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.6', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.29_111111111111_latch_7', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Latch 7', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'latch_id', - 'unique_id': '/29.111111111111/latch.7', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[29.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.0', - 'friendly_name': '29.111111111111 Programmed input-output 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.1', - 'friendly_name': '29.111111111111 Programmed input-output 1', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.2', - 'friendly_name': '29.111111111111 Programmed input-output 2', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.3', - 'friendly_name': '29.111111111111 Programmed input-output 3', - 'raw_value': None, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.4', - 'friendly_name': '29.111111111111 Programmed input-output 4', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.5', - 'friendly_name': '29.111111111111 Programmed input-output 5', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.6', - 'friendly_name': '29.111111111111 Programmed input-output 6', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_6', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/PIO.7', - 'friendly_name': '29.111111111111 Programmed input-output 7', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_programmed_input_output_7', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.0', - 'friendly_name': '29.111111111111 Latch 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.1', - 'friendly_name': '29.111111111111 Latch 1', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.2', - 'friendly_name': '29.111111111111 Latch 2', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.3', - 'friendly_name': '29.111111111111 Latch 3', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.4', - 'friendly_name': '29.111111111111 Latch 4', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_4', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.5', - 'friendly_name': '29.111111111111 Latch 5', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_5', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.6', - 'friendly_name': '29.111111111111 Latch 6', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_6', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/29.111111111111/latch.7', - 'friendly_name': '29.111111111111 Latch 7', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.29_111111111111_latch_7', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) -# --- -# name: test_switches[30.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '30.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2760', - 'model_id': None, - 'name': '30.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[30.111111111111].1 - list([ - ]) -# --- -# name: test_switches[30.111111111111].2 - list([ - ]) -# --- -# name: test_switches[3A.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '3A.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2413', - 'model_id': None, - 'name': '3A.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[3A.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.3a_111111111111_programmed_input_output_a', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output A', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/3A.111111111111/PIO.A', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.3a_111111111111_programmed_input_output_b', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Programmed input-output B', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'pio_id', - 'unique_id': '/3A.111111111111/PIO.B', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[3A.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/3A.111111111111/PIO.A', - 'friendly_name': '3A.111111111111 Programmed input-output A', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.3a_111111111111_programmed_input_output_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/3A.111111111111/PIO.B', - 'friendly_name': '3A.111111111111 Programmed input-output B', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.3a_111111111111_programmed_input_output_b', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) -# --- -# name: test_switches[3B.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '3B.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS1825', - 'model_id': None, - 'name': '3B.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[3B.111111111111].1 - list([ - ]) -# --- -# name: test_switches[3B.111111111111].2 - list([ - ]) -# --- -# name: test_switches[42.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '42.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS28EA00', - 'model_id': None, - 'name': '42.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[42.111111111111].1 - list([ - ]) -# --- -# name: test_switches[42.111111111111].2 - list([ - ]) -# --- -# name: test_switches[7E.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '7E.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Embedded Data Systems', - 'model': 'EDS0068', - 'model_id': None, - 'name': '7E.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[7E.111111111111].1 - list([ - ]) -# --- -# name: test_switches[7E.111111111111].2 - list([ - ]) -# --- -# name: test_switches[7E.222222222222] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - '7E.222222222222', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Embedded Data Systems', - 'model': 'EDS0066', - 'model_id': None, - 'name': '7E.222222222222', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[7E.222222222222].1 - list([ - ]) -# --- -# name: test_switches[7E.222222222222].2 - list([ - ]) -# --- -# name: test_switches[A6.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'A6.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Maxim Integrated', - 'model': 'DS2438', - 'model_id': None, - 'name': 'A6.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[A6.111111111111].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.a6_111111111111_current_a_d_control', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Current A/D control', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'iad', - 'unique_id': '/A6.111111111111/IAD', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[A6.111111111111].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/A6.111111111111/IAD', - 'friendly_name': 'A6.111111111111 Current A/D control', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.a6_111111111111_current_a_d_control', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - ]) -# --- -# name: test_switches[EF.111111111111] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111111', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HobbyBoards_EF', - 'model_id': None, - 'name': 'EF.111111111111', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[EF.111111111111].1 - list([ - ]) -# --- -# name: test_switches[EF.111111111111].2 - list([ - ]) -# --- -# name: test_switches[EF.111111111112] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111112', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HB_MOISTURE_METER', - 'model_id': None, - 'name': 'EF.111111111112', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[EF.111111111112].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Leaf sensor 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'leaf_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_leaf.0', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Leaf sensor 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'leaf_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_leaf.1', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Leaf sensor 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'leaf_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_leaf.2', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Leaf sensor 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'leaf_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_leaf.3', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Moisture sensor 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'moisture_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_moisture.0', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Moisture sensor 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'moisture_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_moisture.1', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Moisture sensor 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'moisture_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_moisture.2', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Moisture sensor 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'moisture_sensor_id', - 'unique_id': '/EF.111111111112/moisture/is_moisture.3', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[EF.111111111112].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_leaf.0', - 'friendly_name': 'EF.111111111112 Leaf sensor 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_leaf.1', - 'friendly_name': 'EF.111111111112 Leaf sensor 1', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_leaf.2', - 'friendly_name': 'EF.111111111112 Leaf sensor 2', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_leaf.3', - 'friendly_name': 'EF.111111111112 Leaf sensor 3', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_leaf_sensor_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_moisture.0', - 'friendly_name': 'EF.111111111112 Moisture sensor 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_moisture.1', - 'friendly_name': 'EF.111111111112 Moisture sensor 1', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_moisture.2', - 'friendly_name': 'EF.111111111112 Moisture sensor 2', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111112/moisture/is_moisture.3', - 'friendly_name': 'EF.111111111112 Moisture sensor 3', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111112_moisture_sensor_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) -# --- -# name: test_switches[EF.111111111113] - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'onewire', - 'EF.111111111113', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'Hobby Boards', - 'model': 'HB_HUB', - 'model_id': None, - 'name': 'EF.111111111113', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_switches[EF.111111111113].1 - list([ - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111113_hub_branch_0', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hub branch 0', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_branch_id', - 'unique_id': '/EF.111111111113/hub/branch.0', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111113_hub_branch_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hub branch 1', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_branch_id', - 'unique_id': '/EF.111111111113/hub/branch.1', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111113_hub_branch_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hub branch 2', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_branch_id', - 'unique_id': '/EF.111111111113/hub/branch.2', - 'unit_of_measurement': None, - }), - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': , - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.ef_111111111113_hub_branch_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Hub branch 3', - 'platform': 'onewire', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hub_branch_id', - 'unique_id': '/EF.111111111113/hub/branch.3', - 'unit_of_measurement': None, - }), - ]) -# --- -# name: test_switches[EF.111111111113].2 - list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111113/hub/branch.0', - 'friendly_name': 'EF.111111111113 Hub branch 0', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111113_hub_branch_0', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111113/hub/branch.1', - 'friendly_name': 'EF.111111111113 Hub branch 1', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111113_hub_branch_1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111113/hub/branch.2', - 'friendly_name': 'EF.111111111113 Hub branch 2', - 'raw_value': 1.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111113_hub_branch_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_file': '/EF.111111111113/hub/branch.3', - 'friendly_name': 'EF.111111111113 Hub branch 3', - 'raw_value': 0.0, - }), - 'context': , - 'entity_id': 'switch.ef_111111111113_hub_branch_3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }), - ]) +# name: test_switches[switch.05_111111111111_programmed_input_output-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.05_111111111111_programmed_input_output', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio', + 'unique_id': '/05.111111111111/PIO', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.05_111111111111_programmed_input_output-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/05.111111111111/PIO', + 'friendly_name': '05.111111111111 Programmed input-output', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.05_111111111111_programmed_input_output', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.12_111111111111_latch_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.12_111111111111_latch_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/12.111111111111/latch.A', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.12_111111111111_latch_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/12.111111111111/latch.A', + 'friendly_name': '12.111111111111 Latch A', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.12_111111111111_latch_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.12_111111111111_latch_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.12_111111111111_latch_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/12.111111111111/latch.B', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.12_111111111111_latch_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/12.111111111111/latch.B', + 'friendly_name': '12.111111111111 Latch B', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.12_111111111111_latch_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.12_111111111111_programmed_input_output_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.12_111111111111_programmed_input_output_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/12.111111111111/PIO.A', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.12_111111111111_programmed_input_output_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/12.111111111111/PIO.A', + 'friendly_name': '12.111111111111 Programmed input-output A', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.12_111111111111_programmed_input_output_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.12_111111111111_programmed_input_output_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.12_111111111111_programmed_input_output_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/12.111111111111/PIO.B', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.12_111111111111_programmed_input_output_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/12.111111111111/PIO.B', + 'friendly_name': '12.111111111111 Programmed input-output B', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.12_111111111111_programmed_input_output_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.26_111111111111_current_a_d_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.26_111111111111_current_a_d_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Current A/D control', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'iad', + 'unique_id': '/26.111111111111/IAD', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.26_111111111111_current_a_d_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/26.111111111111/IAD', + 'friendly_name': '26.111111111111 Current A/D control', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.26_111111111111_current_a_d_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.0', + 'friendly_name': '29.111111111111 Latch 0', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.1', + 'friendly_name': '29.111111111111 Latch 1', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.2', + 'friendly_name': '29.111111111111 Latch 2', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.3', + 'friendly_name': '29.111111111111 Latch 3', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 4', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.4', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.4', + 'friendly_name': '29.111111111111 Latch 4', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 5', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.5', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.5', + 'friendly_name': '29.111111111111 Latch 5', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 6', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.6', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.6', + 'friendly_name': '29.111111111111 Latch 6', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_latch_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_latch_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Latch 7', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'latch_id', + 'unique_id': '/29.111111111111/latch.7', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_latch_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/latch.7', + 'friendly_name': '29.111111111111 Latch 7', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_latch_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.0', + 'friendly_name': '29.111111111111 Programmed input-output 0', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.1', + 'friendly_name': '29.111111111111 Programmed input-output 1', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.2', + 'friendly_name': '29.111111111111 Programmed input-output 2', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.3', + 'friendly_name': '29.111111111111 Programmed input-output 3', + 'raw_value': None, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 4', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.4', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.4', + 'friendly_name': '29.111111111111 Programmed input-output 4', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_5', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 5', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.5', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.5', + 'friendly_name': '29.111111111111 Programmed input-output 5', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_6-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_6', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 6', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.6', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_6-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.6', + 'friendly_name': '29.111111111111 Programmed input-output 6', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_6', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_7-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.29_111111111111_programmed_input_output_7', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output 7', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/29.111111111111/PIO.7', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.29_111111111111_programmed_input_output_7-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/29.111111111111/PIO.7', + 'friendly_name': '29.111111111111 Programmed input-output 7', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.29_111111111111_programmed_input_output_7', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.3a_111111111111_programmed_input_output_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.3a_111111111111_programmed_input_output_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output A', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/3A.111111111111/PIO.A', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.3a_111111111111_programmed_input_output_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/3A.111111111111/PIO.A', + 'friendly_name': '3A.111111111111 Programmed input-output A', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.3a_111111111111_programmed_input_output_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.3a_111111111111_programmed_input_output_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.3a_111111111111_programmed_input_output_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Programmed input-output B', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pio_id', + 'unique_id': '/3A.111111111111/PIO.B', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.3a_111111111111_programmed_input_output_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/3A.111111111111/PIO.B', + 'friendly_name': '3A.111111111111 Programmed input-output B', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.3a_111111111111_programmed_input_output_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.a6_111111111111_current_a_d_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.a6_111111111111_current_a_d_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Current A/D control', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'iad', + 'unique_id': '/A6.111111111111/IAD', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.a6_111111111111_current_a_d_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/A6.111111111111/IAD', + 'friendly_name': 'A6.111111111111 Current A/D control', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.a6_111111111111_current_a_d_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaf sensor 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaf_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_leaf.0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_leaf.0', + 'friendly_name': 'EF.111111111112 Leaf sensor 0', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaf sensor 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaf_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_leaf.1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_leaf.1', + 'friendly_name': 'EF.111111111112 Leaf sensor 1', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaf sensor 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaf_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_leaf.2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_leaf.2', + 'friendly_name': 'EF.111111111112 Leaf sensor 2', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaf sensor 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaf_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_leaf.3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_leaf_sensor_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_leaf.3', + 'friendly_name': 'EF.111111111112 Leaf sensor 3', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_leaf_sensor_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Moisture sensor 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_moisture.0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_moisture.0', + 'friendly_name': 'EF.111111111112 Moisture sensor 0', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Moisture sensor 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_moisture.1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_moisture.1', + 'friendly_name': 'EF.111111111112 Moisture sensor 1', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Moisture sensor 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_moisture.2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_moisture.2', + 'friendly_name': 'EF.111111111112 Moisture sensor 2', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Moisture sensor 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'moisture_sensor_id', + 'unique_id': '/EF.111111111112/moisture/is_moisture.3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111112_moisture_sensor_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111112/moisture/is_moisture.3', + 'friendly_name': 'EF.111111111112 Moisture sensor 3', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111112_moisture_sensor_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111113_hub_branch_0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hub branch 0', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_branch_id', + 'unique_id': '/EF.111111111113/hub/branch.0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111113/hub/branch.0', + 'friendly_name': 'EF.111111111113 Hub branch 0', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111113_hub_branch_0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111113_hub_branch_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hub branch 1', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_branch_id', + 'unique_id': '/EF.111111111113/hub/branch.1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111113/hub/branch.1', + 'friendly_name': 'EF.111111111113 Hub branch 1', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111113_hub_branch_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111113_hub_branch_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hub branch 2', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_branch_id', + 'unique_id': '/EF.111111111113/hub/branch.2', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111113/hub/branch.2', + 'friendly_name': 'EF.111111111113 Hub branch 2', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111113_hub_branch_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ef_111111111113_hub_branch_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hub branch 3', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hub_branch_id', + 'unique_id': '/EF.111111111113/hub/branch.3', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[switch.ef_111111111113_hub_branch_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/EF.111111111113/hub/branch.3', + 'friendly_name': 'EF.111111111113 Hub branch 3', + 'raw_value': 0.0, + }), + 'context': , + 'entity_id': 'switch.ef_111111111113_hub_branch_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) # --- diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 31895f705ff..dd2f3874e36 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -3,57 +3,65 @@ from collections.abc import Generator from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.onewire.onewirehub import _DEVICE_SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.fixture(autouse=True) def override_platforms() -> Generator[None]: """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [Platform.BINARY_SENSOR]): + with patch("homeassistant.components.onewire._PLATFORMS", [Platform.BINARY_SENSOR]): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, owproxy: MagicMock, - device_id: str, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test for 1-Wire binary sensors.""" - setup_owproxy_mock_devices(owproxy, Platform.BINARY_SENSOR, [device_id]) + """Test for 1-Wire binary sensor entities.""" + setup_owproxy_mock_devices(owproxy, MOCK_OWPROXY_DEVICES.keys()) await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - # Ensure devices are correctly registered - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize("device_id", ["29.111111111111"]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensors_delayed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_id: str, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test for delayed 1-Wire binary sensor entities.""" + setup_owproxy_mock_devices(owproxy, []) + await hass.config_entries.async_setup(config_entry.entry_id) + + assert not er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + + setup_owproxy_mock_devices(owproxy, [device_id]) + freezer.tick(_DEVICE_SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 8 ) - assert device_entries == snapshot - - # Ensure entities are correctly registered - entity_entries = er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - assert entity_entries == snapshot - - setup_owproxy_mock_devices(owproxy, Platform.BINARY_SENSOR, [device_id]) - # Some entities are disabled, enable them and reload before checking states - for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, disabled_by=None) - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - - # Ensure entity states are correct - states = [hass.states.get(ent.entity_id) for ent in entity_entries] - assert states == snapshot diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 029e1278c86..65bdaafc131 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for 1-Wire config flow.""" +from ipaddress import ip_address from unittest.mock import AsyncMock, patch from pyownet import protocol @@ -11,18 +12,39 @@ from homeassistant.components.onewire.const import ( INPUT_ENTRY_DEVICE_SELECTION, MANUFACTURER_MAXIM, ) -from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.hassio import HassioServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo + +from tests.common import MockConfigEntry + +_HASSIO_DISCOVERY = HassioServiceInfo( + config={"host": "1302b8e0-owserver", "port": 4304, "addon": "owserver (1-wire)"}, + name="owserver (1-wire)", + slug="1302b8e0_owserver", + uuid="e3fa56560d93458b96a594cbcea3017e", +) +_ZEROCONF_DISCOVERY = ZeroconfServiceInfo( + ip_address=ip_address("5.6.7.8"), + ip_addresses=[ip_address("5.6.7.8")], + hostname="ubuntu.local.", + name="OWFS (1-wire) Server", + port=4304, + type="_owserver._tcp.local.", + properties={}, +) pytestmark = pytest.mark.usefixtures("mock_setup_entry") @pytest.fixture async def filled_device_registry( - hass: HomeAssistant, config_entry: ConfigEntry, device_registry: dr.DeviceRegistry + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, ) -> dr.DeviceRegistry: """Fill device registry with mock devices.""" for key in ("28.111111111111", "28.222222222222", "28.222222222223"): @@ -36,13 +58,31 @@ async def filled_device_registry( return device_registry -async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: +async def test_user_flow(hass: HomeAssistant) -> None: """Test user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] is FlowResultType.FORM - assert not result["errors"] + + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + new_entry = result["result"] + assert new_entry.title == "1.2.3.4" + assert new_entry.data == {CONF_HOST: "1.2.3.4", CONF_PORT: 1234} + + +async def test_user_flow_recovery(hass: HomeAssistant) -> None: + """Test user flow recovery after invalid server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) # Invalid server with patch( @@ -54,9 +94,9 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} # Valid server with patch( @@ -67,19 +107,14 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "1.2.3.4" - assert result["data"] == { - CONF_HOST: "1.2.3.4", - CONF_PORT: 1234, - } - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + new_entry = result["result"] + assert new_entry.title == "1.2.3.4" + assert new_entry.data == {CONF_HOST: "1.2.3.4", CONF_PORT: 1234} async def test_user_duplicate( - hass: HomeAssistant, config_entry: ConfigEntry, mock_setup_entry: AsyncMock + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test user duplicate flow.""" await hass.config_entries.async_setup(config_entry.entry_id) @@ -102,9 +137,185 @@ async def test_user_duplicate( assert result["reason"] == "already_configured" +async def test_reconfigure_flow( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_setup_entry: AsyncMock +) -> None: + """Test reconfigure flow.""" + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert not result["errors"] + + # Invalid server + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + side_effect=protocol.ConnError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 2345}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": "cannot_connect"} + + # Valid server + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 2345}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data == {CONF_HOST: "2.3.4.5", CONF_PORT: 2345} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reconfigure_duplicate( + hass: HomeAssistant, config_entry: MockConfigEntry, mock_setup_entry: AsyncMock +) -> None: + """Test reconfigure duplicate flow.""" + other_config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOST: "2.3.4.5", + CONF_PORT: 2345, + }, + entry_id="other", + ) + other_config_entry.add_to_hass(hass) + + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert not result["errors"] + + # Duplicate server + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 2345}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert len(mock_setup_entry.mock_calls) == 0 + assert config_entry.data == {CONF_HOST: "1.2.3.4", CONF_PORT: 1234} + assert other_config_entry.data == {CONF_HOST: "2.3.4.5", CONF_PORT: 2345} + + +async def test_hassio_flow(hass: HomeAssistant) -> None: + """Test HassIO discovery flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_HASSIO}, + data=_HASSIO_DISCOVERY, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert not result["errors"] + + # Cannot connect to server => retry + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + side_effect=protocol.ConnError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert result["errors"] == {"base": "cannot_connect"} + + # Connect OK + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + new_entry = result["result"] + assert new_entry.title == "owserver (1-wire)" + assert new_entry.data == {CONF_HOST: "1302b8e0-owserver", CONF_PORT: 4304} + + +@pytest.mark.usefixtures("config_entry") +async def test_hassio_duplicate(hass: HomeAssistant) -> None: + """Test HassIO discovery duplicate flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_HASSIO}, + data=_HASSIO_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_flow(hass: HomeAssistant) -> None: + """Test zeroconf discovery flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=_ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert not result["errors"] + + # Cannot connect to server => retry + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + side_effect=protocol.ConnError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert result["errors"] == {"base": "cannot_connect"} + + # Connect OK + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + new_entry = result["result"] + assert new_entry.title == "OWFS (1-wire) Server" + assert new_entry.data == {CONF_HOST: "ubuntu.local.", CONF_PORT: 4304} + + +@pytest.mark.usefixtures("config_entry") +async def test_zeroconf_duplicate(hass: HomeAssistant) -> None: + """Test zeroconf discovery duplicate flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=_ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.usefixtures("filled_device_registry") async def test_user_options_clear( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test clearing the options.""" assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -127,8 +338,8 @@ async def test_user_options_clear( @pytest.mark.usefixtures("filled_device_registry") -async def test_user_options_empty_selection( - hass: HomeAssistant, config_entry: ConfigEntry +async def test_user_options_empty_selection_recovery( + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test leaving the selection of devices empty.""" assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -141,7 +352,7 @@ async def test_user_options_empty_selection( "28.222222222223": False, } - # Verify that an empty selection does not modify the options + # Verify that an empty selection shows the form again result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, @@ -150,10 +361,29 @@ async def test_user_options_empty_selection( assert result["step_id"] == "device_selection" assert result["errors"] == {"base": "device_not_selected"} + # Verify that a single selected device to configure comes back as a form with the device to configure + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, + ) + assert result["type"] is FlowResultType.FORM + assert result["description_placeholders"]["sensor_id"] == "28.111111111111" + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature" + ) + @pytest.mark.usefixtures("filled_device_registry") async def test_user_options_set_single( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test configuring a single device.""" # Clear config options to certify functionality when starting from scratch @@ -191,7 +421,7 @@ async def test_user_options_set_single( async def test_user_options_set_multiple( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, filled_device_registry: dr.DeviceRegistry, ) -> None: """Test configuring multiple consecutive devices in a row.""" @@ -254,7 +484,7 @@ async def test_user_options_set_multiple( async def test_user_options_no_devices( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test that options does not change when no devices are available.""" assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index ecdae859597..60b57bd14f7 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -6,12 +6,12 @@ from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from . import setup_owproxy_mock_devices +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -19,35 +19,21 @@ from tests.typing import ClientSessionGenerator @pytest.fixture(autouse=True) def override_platforms() -> Generator[None]: """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): + with patch("homeassistant.components.onewire._PLATFORMS", [Platform.SWITCH]): yield -DEVICE_DETAILS = { - "device_info": { - "identifiers": [["onewire", "EF.111111111113"]], - "manufacturer": "Hobby Boards", - "model": "HB_HUB", - "name": "EF.111111111113", - }, - "family": "EF", - "id": "EF.111111111113", - "path": "/EF.111111111113/", - "type": "HB_HUB", -} - - @pytest.mark.parametrize("device_id", ["EF.111111111113"], indirect=True) async def test_entry_diagnostics( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, hass_client: ClientSessionGenerator, owproxy: MagicMock, device_id: str, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) + setup_owproxy_mock_devices(owproxy, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 82ff75628c2..0748481c40b 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -3,23 +3,30 @@ from copy import deepcopy from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory from pyownet import protocol import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.onewire.const import DOMAIN -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.components.onewire.onewirehub import _DEVICE_SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES +from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import WebSocketGenerator @pytest.mark.usefixtures("owproxy_with_connerror") -async def test_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def test_connect_failure( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test connection failure raises ConfigEntryNotReady.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -29,7 +36,7 @@ async def test_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry) - async def test_listing_failure( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock + hass: HomeAssistant, config_entry: MockConfigEntry, owproxy: MagicMock ) -> None: """Test listing failure raises ConfigEntryNotReady.""" owproxy.return_value.dir.side_effect = protocol.OwnetError() @@ -42,7 +49,7 @@ async def test_listing_failure( @pytest.mark.usefixtures("owproxy") -async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def test_unload_entry(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Test being able to unload an entry.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -57,7 +64,7 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N async def test_update_options( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock + hass: HomeAssistant, config_entry: MockConfigEntry, owproxy: MagicMock ) -> None: """Test update options triggers reload.""" await hass.config_entries.async_setup(config_entry.entry_id) @@ -77,11 +84,56 @@ async def test_update_options( assert owproxy.call_count == 2 -@patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_registry( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test device are correctly registered.""" + setup_owproxy_mock_devices(owproxy, MOCK_OWPROXY_DEVICES.keys()) + await hass.config_entries.async_setup(config_entry.entry_id) + + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + assert device_entries + for device_entry in device_entries: + assert device_entry == snapshot(name=f"{device_entry.name}-entry") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_registry_delayed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test device are correctly registered.""" + setup_owproxy_mock_devices(owproxy, []) + await hass.config_entries.async_setup(config_entry.entry_id) + + assert not dr.async_entries_for_config_entry(device_registry, config_entry.entry_id) + + setup_owproxy_mock_devices(owproxy, ["1F.111111111111"]) + freezer.tick(_DEVICE_SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 2 + ) + + +@patch("homeassistant.components.onewire._PLATFORMS", [Platform.SENSOR]) async def test_registry_cleanup( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, owproxy: MagicMock, hass_ws_client: WebSocketGenerator, ) -> None: @@ -93,12 +145,12 @@ async def test_registry_cleanup( dead_id = "28.111111111111" # Initialise with two components - setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [live_id, dead_id]) + setup_owproxy_mock_devices(owproxy, [live_id, dead_id]) await hass.config_entries.async_setup(entry_id) await hass.async_block_till_done() # Reload with a device no longer on bus - setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [live_id]) + setup_owproxy_mock_devices(owproxy, [live_id]) await hass.config_entries.async_reload(entry_id) await hass.async_block_till_done() assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 2 diff --git a/tests/components/onewire/test_select.py b/tests/components/onewire/test_select.py new file mode 100644 index 00000000000..6e1c3277c73 --- /dev/null +++ b/tests/components/onewire/test_select.py @@ -0,0 +1,96 @@ +"""Tests for 1-Wire selects.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.onewire.onewirehub import _DEVICE_SCAN_INTERVAL +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.onewire._PLATFORMS", [Platform.SELECT]): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_selects( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test for 1-Wire select entities.""" + setup_owproxy_mock_devices(owproxy, MOCK_OWPROXY_DEVICES.keys()) + await hass.config_entries.async_setup(config_entry.entry_id) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize("device_id", ["28.111111111111"]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_selects_delayed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_id: str, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test for delayed 1-Wire select entities.""" + setup_owproxy_mock_devices(owproxy, []) + await hass.config_entries.async_setup(config_entry.entry_id) + + assert not er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + + setup_owproxy_mock_devices(owproxy, [device_id]) + freezer.tick(_DEVICE_SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 1 + ) + + +@pytest.mark.parametrize("device_id", ["28.111111111111"]) +async def test_selection_option_service( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_id: str, +) -> None: + """Test for 1-Wire select option service.""" + setup_owproxy_mock_devices(owproxy, [device_id]) + await hass.config_entries.async_setup(config_entry.entry_id) + + entity_id = "select.28_111111111111_temperature_resolution" + assert hass.states.get(entity_id).state == "12" + + # Test SELECT_OPTION service + owproxy.return_value.read.side_effect = [b" 9"] + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "9"}, + blocking=True, + ) + assert hass.states.get(entity_id).state == "9" diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index ba0e21701f8..f1ef2dfa11b 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -5,68 +5,75 @@ from copy import deepcopy import logging from unittest.mock import MagicMock, _patch_dict, patch +from freezegun.api import FrozenDateTimeFactory from pyownet.protocol import OwnetError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.onewire.onewirehub import _DEVICE_SCAN_INTERVAL from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_owproxy_mock_devices from .const import ATTR_INJECT_READS, MOCK_OWPROXY_DEVICES +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + @pytest.fixture(autouse=True) def override_platforms() -> Generator[None]: """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]): + with patch("homeassistant.components.onewire._PLATFORMS", [Platform.SENSOR]): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, owproxy: MagicMock, - device_id: str, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test for 1-Wire sensors.""" - setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) + """Test for 1-Wire sensor entities.""" + setup_owproxy_mock_devices(owproxy, MOCK_OWPROXY_DEVICES.keys()) await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - # Ensure devices are correctly registered - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize("device_id", ["12.111111111111"]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_delayed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_id: str, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test for delayed 1-Wire sensor entities.""" + setup_owproxy_mock_devices(owproxy, []) + await hass.config_entries.async_setup(config_entry.entry_id) + + assert not er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + + setup_owproxy_mock_devices(owproxy, [device_id]) + freezer.tick(_DEVICE_SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 2 ) - assert device_entries == snapshot - - # Ensure entities are correctly registered - entity_entries = er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - assert entity_entries == snapshot - - setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) - # Some entities are disabled, enable them and reload before checking states - for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, disabled_by=None) - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - - # Ensure entity states are correct - states = [hass.states.get(ent.entity_id) for ent in entity_entries] - assert states == snapshot @pytest.mark.parametrize("device_id", ["12.111111111111"]) async def test_tai8570_sensors( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, owproxy: MagicMock, device_id: str, entity_registry: er.EntityRegistry, @@ -78,11 +85,11 @@ async def test_tai8570_sensors( """ mock_devices = deepcopy(MOCK_OWPROXY_DEVICES) mock_device = mock_devices[device_id] - mock_device[ATTR_INJECT_READS].append(OwnetError) - mock_device[ATTR_INJECT_READS].append(OwnetError) + mock_device[ATTR_INJECT_READS]["/TAI8570/temperature"] = [OwnetError] + mock_device[ATTR_INJECT_READS]["/TAI8570/pressure"] = [OwnetError] with _patch_dict(MOCK_OWPROXY_DEVICES, mock_devices): - setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) + setup_owproxy_mock_devices(owproxy, [device_id]) with caplog.at_level(logging.DEBUG): await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 936e83f66ec..ca13a69e2da 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -3,11 +3,12 @@ from collections.abc import Generator from unittest.mock import MagicMock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.onewire.onewirehub import _DEVICE_SCAN_INTERVAL from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, @@ -16,66 +17,73 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.fixture(autouse=True) def override_platforms() -> Generator[None]: """Override PLATFORMS.""" - with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): + with patch("homeassistant.components.onewire._PLATFORMS", [Platform.SWITCH]): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switches( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, owproxy: MagicMock, - device_id: str, - device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test for 1-Wire switches.""" - setup_owproxy_mock_devices(owproxy, Platform.SWITCH, [device_id]) + """Test for 1-Wire switch entities.""" + setup_owproxy_mock_devices(owproxy, MOCK_OWPROXY_DEVICES.keys()) await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - # Ensure devices are correctly registered - device_entries = dr.async_entries_for_config_entry( - device_registry, config_entry.entry_id + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize("device_id", ["05.111111111111"]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_switches_delayed( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_id: str, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test for delayed 1-Wire switch entities.""" + setup_owproxy_mock_devices(owproxy, []) + await hass.config_entries.async_setup(config_entry.entry_id) + + assert not er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) + + setup_owproxy_mock_devices(owproxy, [device_id]) + freezer.tick(_DEVICE_SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + len(er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)) + == 1 ) - assert device_entries == snapshot - - # Ensure entities are correctly registered - entity_entries = er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - assert entity_entries == snapshot - - setup_owproxy_mock_devices(owproxy, Platform.SWITCH, [device_id]) - # Some entities are disabled, enable them and reload before checking states - for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, disabled_by=None) - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - - # Ensure entity states are correct - states = [hass.states.get(ent.entity_id) for ent in entity_entries] - assert states == snapshot @pytest.mark.parametrize("device_id", ["05.111111111111"]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_switch_toggle( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: MockConfigEntry, owproxy: MagicMock, device_id: str, ) -> None: """Test for 1-Wire switch TOGGLE service.""" - setup_owproxy_mock_devices(owproxy, Platform.SWITCH, [device_id]) + setup_owproxy_mock_devices(owproxy, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onkyo/test_config_flow.py b/tests/components/onkyo/test_config_flow.py index 1ee0bfdf9c5..203cc22cf95 100644 --- a/tests/components/onkyo/test_config_flow.py +++ b/tests/components/onkyo/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components.onkyo import InputSource from homeassistant.components.onkyo.config_flow import OnkyoConfigFlow from homeassistant.components.onkyo.const import ( DOMAIN, + OPTION_INPUT_SOURCES, OPTION_MAX_VOLUME, OPTION_VOLUME_RESOLUTION, ) @@ -17,6 +18,10 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType, InvalidData +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + SsdpServiceInfo, +) from . import ( create_config_entry_from_info, @@ -198,6 +203,152 @@ async def test_discovery_with_one_selected(hass: HomeAssistant) -> None: assert select_result["description_placeholders"]["name"] == "type 42 (host 42)" +async def test_ssdp_discovery_success( + hass: HomeAssistant, default_mock_discovery +) -> None: + """Test SSDP discovery with valid host.""" + discovery_info = SsdpServiceInfo( + ssdp_location="http://192.168.1.100:8080", + upnp={ATTR_UPNP_FRIENDLY_NAME: "Onkyo Receiver"}, + ssdp_usn="uuid:mock_usn", + ssdp_udn="uuid:00000000-0000-0000-0000-000000000000", + ssdp_st="mock_st", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "configure_receiver" + + select_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"volume_resolution": 200, "input_sources": ["TV"]}, + ) + + assert select_result["type"] is FlowResultType.CREATE_ENTRY + assert select_result["data"]["host"] == "192.168.1.100" + assert select_result["result"].unique_id == "id1" + + +async def test_ssdp_discovery_already_configured( + hass: HomeAssistant, default_mock_discovery +) -> None: + """Test SSDP discovery with already configured device.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.1.100"}, + unique_id="id1", + ) + config_entry.add_to_hass(hass) + + discovery_info = SsdpServiceInfo( + ssdp_location="http://192.168.1.100:8080", + upnp={ATTR_UPNP_FRIENDLY_NAME: "Onkyo Receiver"}, + ssdp_usn="uuid:mock_usn", + ssdp_udn="uuid:00000000-0000-0000-0000-000000000000", + ssdp_st="mock_st", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_discovery_host_info_error(hass: HomeAssistant) -> None: + """Test SSDP discovery with host info error.""" + discovery_info = SsdpServiceInfo( + ssdp_location="http://192.168.1.100:8080", + upnp={ATTR_UPNP_FRIENDLY_NAME: "Onkyo Receiver"}, + ssdp_usn="uuid:mock_usn", + ssdp_st="mock_st", + ) + + with patch( + "homeassistant.components.onkyo.receiver.pyeiscp.Connection.discover", + side_effect=OSError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unknown" + + +async def test_ssdp_discovery_host_none_info( + hass: HomeAssistant, stub_mock_discovery +) -> None: + """Test SSDP discovery with host info error.""" + discovery_info = SsdpServiceInfo( + ssdp_location="http://192.168.1.100:8080", + upnp={ATTR_UPNP_FRIENDLY_NAME: "Onkyo Receiver"}, + ssdp_usn="uuid:mock_usn", + ssdp_st="mock_st", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_ssdp_discovery_no_location( + hass: HomeAssistant, default_mock_discovery +) -> None: + """Test SSDP discovery with no location.""" + discovery_info = SsdpServiceInfo( + ssdp_location=None, + upnp={ATTR_UPNP_FRIENDLY_NAME: "Onkyo Receiver"}, + ssdp_usn="uuid:mock_usn", + ssdp_st="mock_st", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unknown" + + +async def test_ssdp_discovery_no_host( + hass: HomeAssistant, default_mock_discovery +) -> None: + """Test SSDP discovery with no host.""" + discovery_info = SsdpServiceInfo( + ssdp_location="http://", + upnp={ATTR_UPNP_FRIENDLY_NAME: "Onkyo Receiver"}, + ssdp_usn="uuid:mock_usn", + ssdp_st="mock_st", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unknown" + + async def test_configure_empty_source_list( hass: HomeAssistant, default_mock_discovery ) -> None: @@ -316,7 +467,7 @@ async def test_reconfigure(hass: HomeAssistant, default_mock_discovery) -> None: await setup_integration(hass, config_entry, receiver_info) old_host = config_entry.data[CONF_HOST] - old_max_volume = config_entry.options[OPTION_MAX_VOLUME] + old_options = config_entry.options result = await config_entry.start_reconfigure_flow(hass) @@ -333,7 +484,7 @@ async def test_reconfigure(hass: HomeAssistant, default_mock_discovery) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - user_input={"volume_resolution": 200, "input_sources": ["TUNER"]}, + user_input={OPTION_VOLUME_RESOLUTION: 200}, ) assert result3["type"] is FlowResultType.ABORT @@ -341,7 +492,10 @@ async def test_reconfigure(hass: HomeAssistant, default_mock_discovery) -> None: assert config_entry.data[CONF_HOST] == old_host assert config_entry.options[OPTION_VOLUME_RESOLUTION] == 200 - assert config_entry.options[OPTION_MAX_VOLUME] == old_max_volume + for option, option_value in old_options.items(): + if option == OPTION_VOLUME_RESOLUTION: + continue + assert config_entry.options[option] == option_value async def test_reconfigure_new_device(hass: HomeAssistant) -> None: @@ -460,8 +614,8 @@ async def test_import_success( "ignore_translations", [ [ # The schema is dynamically created from input sources - "component.onkyo.options.step.init.data.TV", - "component.onkyo.options.step.init.data_description.TV", + "component.onkyo.options.step.names.sections.input_sources.data.TV", + "component.onkyo.options.step.names.sections.input_sources.data_description.TV", ] ], ) @@ -472,23 +626,43 @@ async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry) config_entry = create_empty_config_entry() await setup_integration(hass, config_entry, receiver_info) + old_volume_resolution = config_entry.options[OPTION_VOLUME_RESOLUTION] + result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "max_volume": 42, - "TV": "television", + OPTION_MAX_VOLUME: 42, + OPTION_INPUT_SOURCES: [], + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] == {OPTION_INPUT_SOURCES: "empty_input_source_list"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + OPTION_MAX_VOLUME: 42, + OPTION_INPUT_SOURCES: ["TV"], + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "names" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + OPTION_INPUT_SOURCES: {"TV": "television"}, }, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { - "volume_resolution": 80, - "max_volume": 42.0, - "input_sources": { - "12": "television", - }, + OPTION_VOLUME_RESOLUTION: old_volume_resolution, + OPTION_MAX_VOLUME: 42.0, + OPTION_INPUT_SOURCES: {"12": "television"}, } diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 5c01fb2d200..0bad7050fd9 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -6,13 +6,13 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.onvif import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_DHCP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import ( HOST, @@ -44,10 +44,10 @@ DISCOVERY = [ "MAC": "ff:ee:dd:cc:bb:aa", }, ] -DHCP_DISCOVERY = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY = DhcpServiceInfo( hostname="any", ip="5.6.7.8", macaddress=MAC.lower().replace(":", "") ) -DHCP_DISCOVERY_SAME_IP = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY_SAME_IP = DhcpServiceInfo( hostname="any", ip="1.2.3.4", macaddress=MAC.lower().replace(":", "") ) diff --git a/tests/components/onvif/test_parsers.py b/tests/components/onvif/test_parsers.py index 209e7cbccef..4f7e10abae6 100644 --- a/tests/components/onvif/test_parsers.py +++ b/tests/components/onvif/test_parsers.py @@ -119,7 +119,83 @@ async def test_line_detector_crossed(hass: HomeAssistant) -> None: ) -async def test_tapo_vehicle(hass: HomeAssistant) -> None: +async def test_tapo_line_crossed(hass: HomeAssistant) -> None: + """Tests tns1:RuleEngine/CellMotionDetector/LineCross.""" + event = await get_event( + { + "SubscriptionReference": { + "Address": { + "_value_1": "http://CAMERA_LOCAL_IP:2020/event-0_2020", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Topic": { + "_value_1": "tns1:RuleEngine/CellMotionDetector/LineCross", + "Dialect": "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet", + "_attr_1": {}, + }, + "ProducerReference": { + "Address": { + "_value_1": "http://CAMERA_LOCAL_IP:5656/event", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Message": { + "_value_1": { + "Source": { + "SimpleItem": [ + { + "Name": "VideoSourceConfigurationToken", + "Value": "vsconf", + }, + { + "Name": "VideoAnalyticsConfigurationToken", + "Value": "VideoAnalyticsToken", + }, + {"Name": "Rule", "Value": "MyLineCrossDetectorRule"}, + ], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Key": None, + "Data": { + "SimpleItem": [{"Name": "IsLineCross", "Value": "true"}], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Extension": None, + "UtcTime": datetime.datetime( + 2025, 1, 3, 21, 5, 14, tzinfo=datetime.UTC + ), + "PropertyOperation": "Changed", + "_attr_1": {}, + } + }, + } + ) + + assert event is not None + assert event.name == "Line Detector Crossed" + assert event.platform == "binary_sensor" + assert event.device_class == "motion" + assert event.value + assert event.uid == ( + f"{TEST_UID}_tns1:RuleEngine/CellMotionDetector/" + "LineCross_VideoSourceToken_VideoAnalyticsToken_MyLineCrossDetectorRule" + ) + + +async def test_tapo_tpsmartevent_vehicle(hass: HomeAssistant) -> None: """Tests tns1:RuleEngine/TPSmartEventDetector/TPSmartEvent - vehicle.""" event = await get_event( { @@ -198,7 +274,83 @@ async def test_tapo_vehicle(hass: HomeAssistant) -> None: ) -async def test_tapo_person(hass: HomeAssistant) -> None: +async def test_tapo_cellmotiondetector_vehicle(hass: HomeAssistant) -> None: + """Tests tns1:RuleEngine/CellMotionDetector/TpSmartEvent - vehicle.""" + event = await get_event( + { + "SubscriptionReference": { + "Address": { + "_value_1": "http://CAMERA_LOCAL_IP:2020/event-0_2020", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Topic": { + "_value_1": "tns1:RuleEngine/CellMotionDetector/TpSmartEvent", + "Dialect": "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet", + "_attr_1": {}, + }, + "ProducerReference": { + "Address": { + "_value_1": "http://CAMERA_LOCAL_IP:5656/event", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Message": { + "_value_1": { + "Source": { + "SimpleItem": [ + { + "Name": "VideoSourceConfigurationToken", + "Value": "vsconf", + }, + { + "Name": "VideoAnalyticsConfigurationToken", + "Value": "VideoAnalyticsToken", + }, + {"Name": "Rule", "Value": "MyTPSmartEventDetectorRule"}, + ], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Key": None, + "Data": { + "SimpleItem": [{"Name": "IsVehicle", "Value": "true"}], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Extension": None, + "UtcTime": datetime.datetime( + 2025, 1, 5, 14, 2, 9, tzinfo=datetime.UTC + ), + "PropertyOperation": "Changed", + "_attr_1": {}, + } + }, + } + ) + + assert event is not None + assert event.name == "Vehicle Detection" + assert event.platform == "binary_sensor" + assert event.device_class == "motion" + assert event.value + assert event.uid == ( + f"{TEST_UID}_tns1:RuleEngine/CellMotionDetector/" + "TpSmartEvent_VideoSourceToken_VideoAnalyticsToken_MyTPSmartEventDetectorRule" + ) + + +async def test_tapo_tpsmartevent_person(hass: HomeAssistant) -> None: """Tests tns1:RuleEngine/TPSmartEventDetector/TPSmartEvent - person.""" event = await get_event( { @@ -274,6 +426,310 @@ async def test_tapo_person(hass: HomeAssistant) -> None: ) +async def test_tapo_tpsmartevent_pet(hass: HomeAssistant) -> None: + """Tests tns1:RuleEngine/TPSmartEventDetector/TPSmartEvent - pet.""" + event = await get_event( + { + "SubscriptionReference": { + "Address": { + "_value_1": "http://192.168.56.63:2020/event-0_2020", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Topic": { + "_value_1": "tns1:RuleEngine/TPSmartEventDetector/TPSmartEvent", + "Dialect": "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet", + "_attr_1": {}, + }, + "ProducerReference": { + "Address": { + "_value_1": "http://192.168.56.63:5656/event", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Message": { + "_value_1": { + "Source": { + "SimpleItem": [ + { + "Name": "VideoSourceConfigurationToken", + "Value": "vsconf", + }, + { + "Name": "VideoAnalyticsConfigurationToken", + "Value": "VideoAnalyticsToken", + }, + {"Name": "Rule", "Value": "MyTPSmartEventDetectorRule"}, + ], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Key": None, + "Data": { + "SimpleItem": [{"Name": "IsPet", "Value": "true"}], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Extension": None, + "UtcTime": datetime.datetime( + 2025, 1, 22, 13, 24, 57, tzinfo=datetime.UTC + ), + "PropertyOperation": "Changed", + "_attr_1": {}, + } + }, + } + ) + + assert event is not None + assert event.name == "Pet Detection" + assert event.platform == "binary_sensor" + assert event.device_class == "motion" + assert event.value + assert event.uid == ( + f"{TEST_UID}_tns1:RuleEngine/TPSmartEventDetector/" + "TPSmartEvent_VideoSourceToken_VideoAnalyticsToken_MyTPSmartEventDetectorRule" + ) + + +async def test_tapo_cellmotiondetector_person(hass: HomeAssistant) -> None: + """Tests tns1:RuleEngine/CellMotionDetector/People - person.""" + event = await get_event( + { + "SubscriptionReference": { + "Address": { + "_value_1": "http://192.168.56.63:2020/event-0_2020", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Topic": { + "_value_1": "tns1:RuleEngine/CellMotionDetector/People", + "Dialect": "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet", + "_attr_1": {}, + }, + "ProducerReference": { + "Address": { + "_value_1": "http://192.168.56.63:5656/event", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Message": { + "_value_1": { + "Source": { + "SimpleItem": [ + { + "Name": "VideoSourceConfigurationToken", + "Value": "vsconf", + }, + { + "Name": "VideoAnalyticsConfigurationToken", + "Value": "VideoAnalyticsToken", + }, + {"Name": "Rule", "Value": "MyPeopleDetectorRule"}, + ], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Key": None, + "Data": { + "SimpleItem": [{"Name": "IsPeople", "Value": "true"}], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Extension": None, + "UtcTime": datetime.datetime( + 2025, 1, 3, 20, 9, 22, tzinfo=datetime.UTC + ), + "PropertyOperation": "Changed", + "_attr_1": {}, + } + }, + } + ) + + assert event is not None + assert event.name == "Person Detection" + assert event.platform == "binary_sensor" + assert event.device_class == "motion" + assert event.value + assert event.uid == ( + f"{TEST_UID}_tns1:RuleEngine/CellMotionDetector/" + "People_VideoSourceToken_VideoAnalyticsToken_MyPeopleDetectorRule" + ) + + +async def test_tapo_tamper(hass: HomeAssistant) -> None: + """Tests tns1:RuleEngine/CellMotionDetector/Tamper - tamper.""" + event = await get_event( + { + "SubscriptionReference": { + "Address": { + "_value_1": "http://CAMERA_LOCAL_IP:2020/event-0_2020", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Topic": { + "_value_1": "tns1:RuleEngine/CellMotionDetector/Tamper", + "Dialect": "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet", + "_attr_1": {}, + }, + "ProducerReference": { + "Address": { + "_value_1": "http://CAMERA_LOCAL_IP:5656/event", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Message": { + "_value_1": { + "Source": { + "SimpleItem": [ + { + "Name": "VideoSourceConfigurationToken", + "Value": "vsconf", + }, + { + "Name": "VideoAnalyticsConfigurationToken", + "Value": "VideoAnalyticsToken", + }, + {"Name": "Rule", "Value": "MyTamperDetectorRule"}, + ], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Key": None, + "Data": { + "SimpleItem": [{"Name": "IsTamper", "Value": "true"}], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Extension": None, + "UtcTime": datetime.datetime( + 2025, 1, 5, 21, 1, 5, tzinfo=datetime.UTC + ), + "PropertyOperation": "Changed", + "_attr_1": {}, + } + }, + } + ) + + assert event is not None + assert event.name == "Tamper Detection" + assert event.platform == "binary_sensor" + assert event.device_class == "tamper" + assert event.value + assert event.uid == ( + f"{TEST_UID}_tns1:RuleEngine/CellMotionDetector/" + "Tamper_VideoSourceToken_VideoAnalyticsToken_MyTamperDetectorRule" + ) + + +async def test_tapo_intrusion(hass: HomeAssistant) -> None: + """Tests tns1:RuleEngine/CellMotionDetector/Intrusion - intrusion.""" + event = await get_event( + { + "SubscriptionReference": { + "Address": { + "_value_1": "http://192.168.100.155:2020/event-0_2020", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Topic": { + "_value_1": "tns1:RuleEngine/CellMotionDetector/Intrusion", + "Dialect": "http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet", + "_attr_1": {}, + }, + "ProducerReference": { + "Address": { + "_value_1": "http://192.168.100.155:5656/event", + "_attr_1": None, + }, + "ReferenceParameters": None, + "Metadata": None, + "_value_1": None, + "_attr_1": None, + }, + "Message": { + "_value_1": { + "Source": { + "SimpleItem": [ + { + "Name": "VideoSourceConfigurationToken", + "Value": "vsconf", + }, + { + "Name": "VideoAnalyticsConfigurationToken", + "Value": "VideoAnalyticsToken", + }, + {"Name": "Rule", "Value": "MyIntrusionDetectorRule"}, + ], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Key": None, + "Data": { + "SimpleItem": [{"Name": "IsIntrusion", "Value": "true"}], + "ElementItem": [], + "Extension": None, + "_attr_1": None, + }, + "Extension": None, + "UtcTime": datetime.datetime( + 2025, 1, 11, 10, 40, 45, tzinfo=datetime.UTC + ), + "PropertyOperation": "Changed", + "_attr_1": {}, + } + }, + } + ) + + assert event is not None + assert event.name == "Intrusion Detection" + assert event.platform == "binary_sensor" + assert event.device_class == "safety" + assert event.value + assert event.uid == ( + f"{TEST_UID}_tns1:RuleEngine/CellMotionDetector/" + "Intrusion_VideoSourceToken_VideoAnalyticsToken_MyIntrusionDetectorRule" + ) + + async def test_tapo_missing_attributes(hass: HomeAssistant) -> None: """Tests async_parse_tplink_detector with missing fields.""" event = await get_event( diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py index 22138846915..66670782265 100644 --- a/tests/components/open_meteo/conftest.py +++ b/tests/components/open_meteo/conftest.py @@ -43,7 +43,7 @@ def mock_open_meteo(request: pytest.FixtureRequest) -> Generator[MagicMock]: forecast = Forecast.from_json(load_fixture(fixture, DOMAIN)) with patch( - "homeassistant.components.open_meteo.OpenMeteo", autospec=True + "homeassistant.components.open_meteo.coordinator.OpenMeteo", autospec=True ) as open_meteo_mock: open_meteo = open_meteo_mock.return_value open_meteo.forecast.return_value = forecast diff --git a/tests/components/open_meteo/snapshots/test_weather.ambr b/tests/components/open_meteo/snapshots/test_weather.ambr new file mode 100644 index 00000000000..dd5beb56d77 --- /dev/null +++ b/tests/components/open_meteo/snapshots/test_weather.ambr @@ -0,0 +1,1070 @@ +# serializer version: 1 +# name: test_forecast_service[forecast_daily] + dict({ + 'weather.home': dict({ + 'forecast': list([ + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-24T00:00:00+00:00', + 'precipitation': 0.19, + 'temperature': 7.6, + 'templow': 5.5, + 'wind_bearing': 251, + 'wind_speed': 10.9, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-25T00:00:00+00:00', + 'precipitation': 0.29, + 'temperature': 5.4, + 'templow': 0.2, + 'wind_bearing': 210, + 'wind_speed': 12.9, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-26T00:00:00+00:00', + 'precipitation': 0.76, + 'temperature': 4.8, + 'templow': 1.8, + 'wind_bearing': 230, + 'wind_speed': 14.8, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-27T00:00:00+00:00', + 'precipitation': 0.12, + 'temperature': 4.5, + 'templow': -0.1, + 'wind_bearing': 143, + 'wind_speed': 10.7, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-28T00:00:00+00:00', + 'precipitation': 0.15, + 'temperature': 3.4, + 'templow': -0.2, + 'wind_bearing': 143, + 'wind_speed': 7.0, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-11-29T00:00:00+00:00', + 'precipitation': 0.64, + 'temperature': 2.2, + 'templow': -0.5, + 'wind_bearing': 248, + 'wind_speed': 13.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T00:00:00+00:00', + 'precipitation': 1.74, + 'temperature': 3.0, + 'templow': -0.3, + 'wind_bearing': 256, + 'wind_speed': 16.1, + }), + ]), + }), + }) +# --- +# name: test_forecast_service[forecast_hourly] + dict({ + 'weather.home': dict({ + 'forecast': list([ + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T03:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.6, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-24T04:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 6.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T05:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 6.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T06:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T07:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T08:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T09:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T10:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T11:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T12:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 7.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T13:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 7.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T14:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 7.6, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-24T15:00:00+00:00', + 'precipitation': 0.06, + 'temperature': 7.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-24T16:00:00+00:00', + 'precipitation': 0.06, + 'temperature': 7.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T17:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T18:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T19:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T20:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 6.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T21:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T22:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-24T23:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T00:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T01:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T02:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 5.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T03:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.9, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-25T04:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.2, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-25T05:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.2, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-25T06:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.2, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-25T07:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.5, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-25T08:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T09:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T10:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T11:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T12:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.7, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T13:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T14:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 4.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-25T15:00:00+00:00', + 'precipitation': 0.07, + 'temperature': 4.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T16:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T17:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T18:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.9, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-25T19:00:00+00:00', + 'precipitation': 0.09, + 'temperature': 3.9, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-25T20:00:00+00:00', + 'precipitation': 0.09, + 'temperature': 4.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T21:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 3.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T22:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-25T23:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T00:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.7, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T01:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T02:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T03:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T04:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T05:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T06:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T07:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T08:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T09:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T10:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T11:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T12:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T13:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T14:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T15:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 4.6, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-26T16:00:00+00:00', + 'precipitation': 0.1, + 'temperature': 4.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-26T17:00:00+00:00', + 'precipitation': 0.3, + 'temperature': 3.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-26T18:00:00+00:00', + 'precipitation': 0.2, + 'temperature': 3.3, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-26T19:00:00+00:00', + 'precipitation': 0.15, + 'temperature': 3.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T20:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.7, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T21:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T22:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-26T23:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T00:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 1.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T01:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T02:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.7, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T03:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T04:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T05:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T06:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.2, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-27T07:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T08:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T09:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T10:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.6, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-27T11:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.7, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-27T12:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.9, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-27T13:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.0, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-27T14:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.5, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-27T15:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 4.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T16:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-27T17:00:00+00:00', + 'precipitation': 0.1, + 'temperature': 3.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T18:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 2.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T19:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T20:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T21:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T22:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-27T23:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.0, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-28T00:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.6, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-28T01:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T02:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T03:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T04:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T05:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T06:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T07:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T08:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T09:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T10:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T11:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T12:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T13:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T14:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T15:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T16:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 3.2, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-28T17:00:00+00:00', + 'precipitation': 0.05, + 'temperature': 3.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-28T18:00:00+00:00', + 'precipitation': 0.05, + 'temperature': 2.7, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-28T19:00:00+00:00', + 'precipitation': 0.05, + 'temperature': 2.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T20:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T21:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.5, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T22:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-28T23:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T00:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T01:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.6, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T02:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T03:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T04:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T05:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T06:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T07:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.5, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-11-29T08:00:00+00:00', + 'precipitation': 0.01, + 'temperature': -0.5, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-11-29T09:00:00+00:00', + 'precipitation': 0.01, + 'temperature': -0.5, + }), + dict({ + 'condition': 'snowy', + 'datetime': '2021-11-29T10:00:00+00:00', + 'precipitation': 0.01, + 'temperature': -0.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T11:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 0.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T12:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 1.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T13:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 2.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T14:00:00+00:00', + 'precipitation': 0.02, + 'temperature': 2.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T15:00:00+00:00', + 'precipitation': 0.02, + 'temperature': 2.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T16:00:00+00:00', + 'precipitation': 0.02, + 'temperature': 1.7, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-29T17:00:00+00:00', + 'precipitation': 0.13, + 'temperature': 1.4, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-29T18:00:00+00:00', + 'precipitation': 0.13, + 'temperature': 1.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-29T19:00:00+00:00', + 'precipitation': 0.13, + 'temperature': 0.7, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T20:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.0, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T21:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T22:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-29T23:00:00+00:00', + 'precipitation': 0.07, + 'temperature': 1.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T00:00:00+00:00', + 'precipitation': 0.07, + 'temperature': 1.7, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T01:00:00+00:00', + 'precipitation': 0.07, + 'temperature': 1.6, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T02:00:00+00:00', + 'precipitation': 0.16, + 'temperature': 1.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T03:00:00+00:00', + 'precipitation': 0.16, + 'temperature': 1.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T04:00:00+00:00', + 'precipitation': 0.16, + 'temperature': 1.3, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T05:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 1.1, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T06:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 0.8, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T07:00:00+00:00', + 'precipitation': 0.01, + 'temperature': 0.5, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-30T08:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.1, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-30T09:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.2, + }), + dict({ + 'condition': 'partlycloudy', + 'datetime': '2021-11-30T10:00:00+00:00', + 'precipitation': 0.0, + 'temperature': -0.3, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-30T11:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 0.2, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-30T12:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.0, + }), + dict({ + 'condition': 'sunny', + 'datetime': '2021-11-30T13:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 1.9, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T14:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.2, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T15:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.4, + }), + dict({ + 'condition': 'cloudy', + 'datetime': '2021-11-30T16:00:00+00:00', + 'precipitation': 0.0, + 'temperature': 2.6, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T17:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 2.6, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T18:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 2.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T19:00:00+00:00', + 'precipitation': 0.03, + 'temperature': 2.4, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T20:00:00+00:00', + 'precipitation': 0.04, + 'temperature': 2.5, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T21:00:00+00:00', + 'precipitation': 0.04, + 'temperature': 2.8, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T22:00:00+00:00', + 'precipitation': 0.04, + 'temperature': 3.0, + }), + dict({ + 'condition': 'rainy', + 'datetime': '2021-11-30T23:00:00+00:00', + 'precipitation': 0.88, + 'temperature': 3.0, + }), + ]), + }), + }) +# --- diff --git a/tests/components/open_meteo/test_init.py b/tests/components/open_meteo/test_init.py index 19efcbec35c..39e3666ef7f 100644 --- a/tests/components/open_meteo/test_init.py +++ b/tests/components/open_meteo/test_init.py @@ -33,7 +33,7 @@ async def test_load_unload_config_entry( @patch( - "homeassistant.components.open_meteo.OpenMeteo.forecast", + "homeassistant.components.open_meteo.coordinator.OpenMeteo.forecast", side_effect=OpenMeteoConnectionError, ) async def test_config_entry_not_ready( diff --git a/tests/components/open_meteo/test_weather.py b/tests/components/open_meteo/test_weather.py new file mode 100644 index 00000000000..b43385c924a --- /dev/null +++ b/tests/components/open_meteo/test_weather.py @@ -0,0 +1,46 @@ +"""Test for the open meteo weather entity.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.weather import ( + DOMAIN as WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2021-11-24T03:00:00+00:00") +async def test_forecast_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_open_meteo: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test forecast service.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, + {ATTR_ENTITY_ID: "weather.home", "type": "daily"}, + blocking=True, + return_response=True, + ) + assert response == snapshot(name="forecast_daily") + + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, + {ATTR_ENTITY_ID: "weather.home", "type": "hourly"}, + blocking=True, + return_response=True, + ) + assert response == snapshot(name="forecast_hourly") diff --git a/tests/components/openai_conversation/snapshots/test_conversation.ambr b/tests/components/openai_conversation/snapshots/test_conversation.ambr index eaa3a9de64c..77c28de2773 100644 --- a/tests/components/openai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/openai_conversation/snapshots/test_conversation.ambr @@ -1,34 +1,50 @@ # serializer version: 1 -# name: test_unknown_hass_api - dict({ - 'conversation_id': None, - 'response': IntentResponse( - card=dict({ - }), - error_code=, - failed_results=list([ - ]), - intent=None, - intent_targets=list([ - ]), - language='en', - matched_states=list([ - ]), - reprompt=dict({ - }), - response_type=, - speech=dict({ - 'plain': dict({ - 'extra_data': None, - 'speech': 'Error preparing LLM API', +# name: test_function_call + list([ + dict({ + 'content': 'Please call the test function', + 'role': 'user', + }), + dict({ + 'agent_id': 'conversation.openai', + 'content': None, + 'role': 'assistant', + 'tool_calls': list([ + dict({ + 'id': 'call_call_1', + 'tool_args': dict({ + 'param1': 'call1', + }), + 'tool_name': 'test_tool', + }), + dict({ + 'id': 'call_call_2', + 'tool_args': dict({ + 'param1': 'call2', + }), + 'tool_name': 'test_tool', }), - }), - speech_slots=dict({ - }), - success_results=list([ ]), - unmatched_states=list([ - ]), - ), - }) + }), + dict({ + 'agent_id': 'conversation.openai', + 'role': 'tool_result', + 'tool_call_id': 'call_call_1', + 'tool_name': 'test_tool', + 'tool_result': 'value1', + }), + dict({ + 'agent_id': 'conversation.openai', + 'role': 'tool_result', + 'tool_call_id': 'call_call_2', + 'tool_name': 'test_tool', + 'tool_result': 'value2', + }), + dict({ + 'agent_id': 'conversation.openai', + 'content': 'Cool', + 'role': 'assistant', + 'tool_calls': None, + }), + ]) # --- diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index f5017c124b1..90a08471f39 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -12,12 +12,14 @@ from homeassistant.components.openai_conversation.const import ( CONF_CHAT_MODEL, CONF_MAX_TOKENS, CONF_PROMPT, + CONF_REASONING_EFFORT, CONF_RECOMMENDED, CONF_TEMPERATURE, CONF_TOP_P, DOMAIN, RECOMMENDED_CHAT_MODEL, RECOMMENDED_MAX_TOKENS, + RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TOP_P, ) from homeassistant.const import CONF_LLM_HASS_API @@ -88,6 +90,27 @@ async def test_options( assert options["data"][CONF_CHAT_MODEL] == RECOMMENDED_CHAT_MODEL +async def test_options_unsupported_model( + hass: HomeAssistant, mock_config_entry, mock_init_component +) -> None: + """Test the options form giving error about models not supported.""" + options_flow = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + result = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + { + CONF_RECOMMENDED: False, + CONF_PROMPT: "Speak like a pirate", + CONF_CHAT_MODEL: "o1-mini", + CONF_LLM_HASS_API: "assist", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"chat_model": "model_not_supported"} + + @pytest.mark.parametrize( ("side_effect", "error"), [ @@ -148,6 +171,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, CONF_TOP_P: RECOMMENDED_TOP_P, CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_REASONING_EFFORT: RECOMMENDED_REASONING_EFFORT, }, ), ( @@ -158,6 +182,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> Non CONF_CHAT_MODEL: RECOMMENDED_CHAT_MODEL, CONF_TOP_P: RECOMMENDED_TOP_P, CONF_MAX_TOKENS: RECOMMENDED_MAX_TOKENS, + CONF_REASONING_EFFORT: RECOMMENDED_REASONING_EFFORT, }, { CONF_RECOMMENDED: True, diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index e0665bc449f..2c956b7e63f 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -1,30 +1,70 @@ """Tests for the OpenAI integration.""" -from unittest.mock import AsyncMock, Mock, patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch -from freezegun import freeze_time from httpx import Response from openai import RateLimitError -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from openai.types.chat.chat_completion_message_tool_call import ( - ChatCompletionMessageToolCall, - Function, +from openai.types.chat.chat_completion_chunk import ( + ChatCompletionChunk, + Choice, + ChoiceDelta, + ChoiceDeltaToolCall, + ChoiceDeltaToolCallFunction, ) -from openai.types.completion_usage import CompletionUsage +import pytest from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import conversation -from homeassistant.components.conversation import trace +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import intent, llm +from homeassistant.helpers import intent from homeassistant.setup import async_setup_component -from homeassistant.util import ulid from tests.common import MockConfigEntry +from tests.components.conversation import ( + MockChatLog, + mock_chat_log, # noqa: F401 +) + +ASSIST_RESPONSE_FINISH = ( + # Assistant message + ChatCompletionChunk( + id="chatcmpl-B", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(content="Cool"))], + ), + # Finish stream + ChatCompletionChunk( + id="chatcmpl-B", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[Choice(index=0, finish_reason="stop", delta=ChoiceDelta())], + ), +) + + +@pytest.fixture +def mock_create_stream() -> Generator[AsyncMock]: + """Mock stream response.""" + + async def mock_generator(stream): + for value in stream: + yield value + + with patch( + "openai.resources.chat.completions.AsyncCompletions.create", + AsyncMock(), + ) as mock_create: + mock_create.side_effect = lambda **kwargs: mock_generator( + mock_create.return_value.pop(0) + ) + + yield mock_create async def test_entity( @@ -57,7 +97,7 @@ async def test_entity( async def test_error_handling( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component ) -> None: - """Test that the default prompt works.""" + """Test that we handle errors when calling completion API.""" with patch( "openai.resources.chat.completions.AsyncCompletions.create", new_callable=AsyncMock, @@ -73,82 +113,6 @@ async def test_error_handling( assert result.response.error_code == "unknown", result -async def test_template_error( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test that template error handling works.""" - hass.config_entries.async_update_entry( - mock_config_entry, - options={ - "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", - }, - ) - with ( - patch( - "openai.resources.models.AsyncModels.list", - ), - patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - ), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - result = await conversation.async_converse( - hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id - ) - - assert result.response.response_type == intent.IntentResponseType.ERROR, result - assert result.response.error_code == "unknown", result - - -async def test_template_variables( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test that template variables work.""" - context = Context(user_id="12345") - mock_user = Mock() - mock_user.id = "12345" - mock_user.name = "Test User" - - hass.config_entries.async_update_entry( - mock_config_entry, - options={ - "prompt": ( - "The user name is {{ user_name }}. " - "The user id is {{ llm_context.context.user_id }}." - ), - }, - ) - with ( - patch( - "openai.resources.models.AsyncModels.list", - ), - patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - ) as mock_create, - patch("homeassistant.auth.AuthManager.async_get_user", return_value=mock_user), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - result = await conversation.async_converse( - hass, "hello", None, context, agent_id=mock_config_entry.entry_id - ) - - assert ( - result.response.response_type == intent.IntentResponseType.ACTION_DONE - ), result - assert ( - "The user name is Test User." - in mock_create.mock_calls[0][2]["messages"][0]["content"] - ) - assert ( - "The user id is 12345." - in mock_create.mock_calls[0][2]["messages"][0]["content"] - ) - - async def test_conversation_agent( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -161,408 +125,299 @@ async def test_conversation_agent( assert agent.supported_languages == "*" -@patch( - "homeassistant.components.openai_conversation.conversation.llm.AssistAPI._async_get_tools" -) async def test_function_call( - mock_get_tools, hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + mock_create_stream: AsyncMock, + mock_chat_log: MockChatLog, # noqa: F811 + snapshot: SnapshotAssertion, ) -> None: """Test function call from the assistant.""" - agent_id = mock_config_entry_with_assist.entry_id - context = Context() - - mock_tool = AsyncMock() - mock_tool.name = "test_tool" - mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} - ) - mock_tool.async_call.return_value = "Test response" - - mock_get_tools.return_value = [mock_tool] - - def completion_result(*args, messages, **kwargs): - for message in messages: - role = message["role"] if isinstance(message, dict) else message.role - if role == "tool": - return ChatCompletion( - id="chatcmpl-1234567890ZYXWVUTSRQPONMLKJIH", - choices=[ - Choice( - finish_reason="stop", - index=0, - message=ChatCompletionMessage( - content="I have successfully called the function", - role="assistant", - function_call=None, - tool_calls=None, - ), - ) - ], - created=1700000000, - model="gpt-4-1106-preview", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 - ), - ) - - return ChatCompletion( - id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", - choices=[ - Choice( - finish_reason="tool_calls", - index=0, - message=ChatCompletionMessage( - content=None, - role="assistant", - function_call=None, - tool_calls=[ - ChatCompletionMessageToolCall( - id="call_AbCdEfGhIjKlMnOpQrStUvWx", - function=Function( - arguments='{"param1":"test_value"}', - name="test_tool", - ), - type="function", - ) - ], - ), - ) - ], - created=1700000000, - model="gpt-4-1106-preview", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 + mock_create_stream.return_value = [ + # Initial conversation + ( + # First tool call + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + id="call_call_1", + index=0, + function=ChoiceDeltaToolCallFunction( + name="test_tool", + arguments=None, + ), + ) + ] + ), + ) + ], ), - ) + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction( + name=None, + arguments='{"para', + ), + ) + ] + ), + ) + ], + ), + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction( + name=None, + arguments='m1":"call1"}', + ), + ) + ] + ), + ) + ], + ), + # Second tool call + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + id="call_call_2", + index=1, + function=ChoiceDeltaToolCallFunction( + name="test_tool", + arguments='{"param1":"call2"}', + ), + ) + ] + ), + ) + ], + ), + # Finish stream + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice(index=0, finish_reason="tool_calls", delta=ChoiceDelta()) + ], + ), + ), + # Response after tool responses + ASSIST_RESPONSE_FINISH, + ] + mock_chat_log.mock_tool_results( + { + "call_call_1": "value1", + "call_call_2": "value2", + } + ) - with ( - patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - side_effect=completion_result, - ) as mock_create, - freeze_time("2024-06-03 23:00:00"), - ): - result = await conversation.async_converse( - hass, - "Please call the test function", - None, - context, - agent_id=agent_id, - ) - - assert ( - "Today's date is 2024-06-03." - in mock_create.mock_calls[1][2]["messages"][0]["content"] + result = await conversation.async_converse( + hass, + "Please call the test function", + mock_chat_log.conversation_id, + Context(), + agent_id="conversation.openai", ) assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert mock_create.mock_calls[1][2]["messages"][3] == { - "role": "tool", - "tool_call_id": "call_AbCdEfGhIjKlMnOpQrStUvWx", - "content": '"Test response"', - } - mock_tool.async_call.assert_awaited_once_with( - hass, - llm.ToolInput( - tool_name="test_tool", - tool_args={"param1": "test_value"}, + # Don't test the prompt, as it's not deterministic + assert mock_chat_log.content[1:] == snapshot + + +@pytest.mark.parametrize( + ("description", "messages"), + [ + ( + "Test function call started with missing arguments", + ( + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + id="call_call_1", + index=0, + function=ChoiceDeltaToolCallFunction( + name="test_tool", + arguments=None, + ), + ) + ] + ), + ) + ], + ), + ChatCompletionChunk( + id="chatcmpl-B", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[Choice(index=0, delta=ChoiceDelta(content="Cool"))], + ), + ), ), - llm.LLMContext( - platform="openai_conversation", - context=context, - user_prompt="Please call the test function", - language="en", - assistant="conversation", - device_id=None, + ( + "Test invalid JSON", + ( + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + id="call_call_1", + index=0, + function=ChoiceDeltaToolCallFunction( + name="test_tool", + arguments=None, + ), + ) + ] + ), + ) + ], + ), + ChatCompletionChunk( + id="chatcmpl-A", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta( + tool_calls=[ + ChoiceDeltaToolCall( + index=0, + function=ChoiceDeltaToolCallFunction( + name=None, + arguments='{"para', + ), + ) + ] + ), + ) + ], + ), + ChatCompletionChunk( + id="chatcmpl-B", + created=1700000000, + model="gpt-4-1106-preview", + object="chat.completion.chunk", + choices=[ + Choice( + index=0, + delta=ChoiceDelta(content="Cool"), + finish_reason="tool_calls", + ) + ], + ), + ), ), - ) - - # Test Conversation tracing - traces = trace.async_get_traces() - assert traces - last_trace = traces[-1].as_dict() - trace_events = last_trace.get("events", []) - assert [event["event_type"] for event in trace_events] == [ - trace.ConversationTraceEventType.ASYNC_PROCESS, - trace.ConversationTraceEventType.AGENT_DETAIL, - trace.ConversationTraceEventType.TOOL_CALL, - ] - # AGENT_DETAIL event contains the raw prompt passed to the model - detail_event = trace_events[1] - assert "Answer in plain text" in detail_event["data"]["messages"][0]["content"] - assert ( - "Today's date is 2024-06-03." - in trace_events[1]["data"]["messages"][0]["content"] - ) - assert [t.name for t in detail_event["data"]["tools"]] == ["test_tool"] - - # Call it again, make sure we have updated prompt - with ( - patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - side_effect=completion_result, - ) as mock_create, - freeze_time("2024-06-04 23:00:00"), - ): - result = await conversation.async_converse( - hass, - "Please call the test function", - None, - context, - agent_id=agent_id, - ) - - assert ( - "Today's date is 2024-06-04." - in mock_create.mock_calls[1][2]["messages"][0]["content"] - ) - # Test old assert message not updated - assert ( - "Today's date is 2024-06-03." - in trace_events[1]["data"]["messages"][0]["content"] - ) - - -@patch( - "homeassistant.components.openai_conversation.conversation.llm.AssistAPI._async_get_tools" + ], ) -async def test_function_exception( - mock_get_tools, +async def test_function_call_invalid( hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + mock_create_stream: AsyncMock, + mock_chat_log: MockChatLog, # noqa: F811 + description: str, + messages: tuple[ChatCompletionChunk], ) -> None: - """Test function call with exception.""" - agent_id = mock_config_entry_with_assist.entry_id - context = Context() + """Test function call containing invalid data.""" + mock_create_stream.return_value = [messages] - mock_tool = AsyncMock() - mock_tool.name = "test_tool" - mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - {vol.Optional("param1", description="Test parameters"): str} - ) - mock_tool.async_call.side_effect = HomeAssistantError("Test tool exception") - - mock_get_tools.return_value = [mock_tool] - - def completion_result(*args, messages, **kwargs): - for message in messages: - role = message["role"] if isinstance(message, dict) else message.role - if role == "tool": - return ChatCompletion( - id="chatcmpl-1234567890ZYXWVUTSRQPONMLKJIH", - choices=[ - Choice( - finish_reason="stop", - index=0, - message=ChatCompletionMessage( - content="There was an error calling the function", - role="assistant", - function_call=None, - tool_calls=None, - ), - ) - ], - created=1700000000, - model="gpt-4-1106-preview", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 - ), - ) - - return ChatCompletion( - id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", - choices=[ - Choice( - finish_reason="tool_calls", - index=0, - message=ChatCompletionMessage( - content=None, - role="assistant", - function_call=None, - tool_calls=[ - ChatCompletionMessageToolCall( - id="call_AbCdEfGhIjKlMnOpQrStUvWx", - function=Function( - arguments='{"param1":"test_value"}', - name="test_tool", - ), - type="function", - ) - ], - ), - ) - ], - created=1700000000, - model="gpt-4-1106-preview", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 - ), - ) - - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - side_effect=completion_result, - ) as mock_create: - result = await conversation.async_converse( + with pytest.raises(ValueError): + await conversation.async_converse( hass, "Please call the test function", - None, - context, - agent_id=agent_id, + "mock-conversation-id", + Context(), + agent_id="conversation.openai", ) - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert mock_create.mock_calls[1][2]["messages"][3] == { - "role": "tool", - "tool_call_id": "call_AbCdEfGhIjKlMnOpQrStUvWx", - "content": '{"error": "HomeAssistantError", "error_text": "Test tool exception"}', - } - mock_tool.async_call.assert_awaited_once_with( - hass, - llm.ToolInput( - tool_name="test_tool", - tool_args={"param1": "test_value"}, - ), - llm.LLMContext( - platform="openai_conversation", - context=context, - user_prompt="Please call the test function", - language="en", - assistant="conversation", - device_id=None, - ), - ) - async def test_assist_api_tools_conversion( hass: HomeAssistant, mock_config_entry_with_assist: MockConfigEntry, mock_init_component, + mock_create_stream, ) -> None: """Test that we are able to convert actual tools from Assist API.""" for component in ( - "intent", - "todo", - "light", - "shopping_list", - "humidifier", + "calendar", "climate", - "media_player", - "vacuum", "cover", + "humidifier", + "intent", + "light", + "media_player", + "script", + "shopping_list", + "todo", + "vacuum", "weather", ): assert await async_setup_component(hass, component, {}) + hass.states.async_set(f"{component}.test", "on") + async_expose_entity(hass, "conversation", f"{component}.test", True) - agent_id = mock_config_entry_with_assist.entry_id - with patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, - return_value=ChatCompletion( - id="chatcmpl-1234567890ABCDEFGHIJKLMNOPQRS", - choices=[ - Choice( - finish_reason="stop", - index=0, - message=ChatCompletionMessage( - content="Hello, how can I help you?", - role="assistant", - function_call=None, - tool_calls=None, - ), - ) - ], - created=1700000000, - model="gpt-3.5-turbo-0613", - object="chat.completion", - system_fingerprint=None, - usage=CompletionUsage( - completion_tokens=9, prompt_tokens=8, total_tokens=17 - ), - ), - ) as mock_create: - await conversation.async_converse( - hass, "hello", None, Context(), agent_id=agent_id - ) + mock_create_stream.return_value = [ASSIST_RESPONSE_FINISH] - tools = mock_create.mock_calls[0][2]["tools"] + await conversation.async_converse( + hass, "hello", None, Context(), agent_id="conversation.openai" + ) + + tools = mock_create_stream.mock_calls[0][2]["tools"] assert tools - - -async def test_unknown_hass_api( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - snapshot: SnapshotAssertion, - mock_init_component, -) -> None: - """Test when we reference an API that no longer exists.""" - hass.config_entries.async_update_entry( - mock_config_entry, - options={ - **mock_config_entry.options, - CONF_LLM_HASS_API: "non-existing", - }, - ) - - await hass.async_block_till_done() - - result = await conversation.async_converse( - hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id - ) - - assert result == snapshot - - -@patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, -) -async def test_conversation_id( - mock_create, - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, -) -> None: - """Test conversation ID is honored.""" - result = await conversation.async_converse( - hass, "hello", None, None, agent_id=mock_config_entry.entry_id - ) - - conversation_id = result.conversation_id - - result = await conversation.async_converse( - hass, "hello", conversation_id, None, agent_id=mock_config_entry.entry_id - ) - - assert result.conversation_id == conversation_id - - unknown_id = ulid.ulid() - - result = await conversation.async_converse( - hass, "hello", unknown_id, None, agent_id=mock_config_entry.entry_id - ) - - assert result.conversation_id != unknown_id - - result = await conversation.async_converse( - hass, "hello", "koala", None, agent_id=mock_config_entry.entry_id - ) - - assert result.conversation_id == "koala" diff --git a/tests/components/openhome/test_config_flow.py b/tests/components/openhome/test_config_flow.py index 7ab1e69106c..6430b8610e9 100644 --- a/tests/components/openhome/test_config_flow.py +++ b/tests/components/openhome/test_config_flow.py @@ -1,12 +1,15 @@ """Tests for the Openhome config flow module.""" -from homeassistant.components import ssdp from homeassistant.components.openhome.const import DOMAIN -from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -14,7 +17,7 @@ MOCK_UDN = "uuid:4c494e4e-1234-ab12-abcd-01234567819f" MOCK_FRIENDLY_NAME = "Test Client" MOCK_SSDP_LOCATION = "http://device:12345/description.xml" -MOCK_DISCOVER = ssdp.SsdpServiceInfo( +MOCK_DISCOVER = SsdpServiceInfo( ssdp_usn="usn", ssdp_st="st", ssdp_location=MOCK_SSDP_LOCATION, @@ -60,7 +63,7 @@ async def test_device_exists(hass: HomeAssistant) -> None: async def test_missing_udn(hass: HomeAssistant) -> None: """Test a ssdp import where discovery is missing udn.""" - broken_discovery = ssdp.SsdpServiceInfo( + broken_discovery = SsdpServiceInfo( ssdp_usn="usn", ssdp_st="st", ssdp_location=MOCK_SSDP_LOCATION, @@ -79,7 +82,7 @@ async def test_missing_udn(hass: HomeAssistant) -> None: async def test_missing_ssdp_location(hass: HomeAssistant) -> None: """Test a ssdp import where discovery is missing udn.""" - broken_discovery = ssdp.SsdpServiceInfo( + broken_discovery = SsdpServiceInfo( ssdp_usn="usn", ssdp_st="st", ssdp_location="", diff --git a/tests/components/opentherm_gw/test_button.py b/tests/components/opentherm_gw/test_button.py index b02a9d9fef0..d8de52559e7 100644 --- a/tests/components/opentherm_gw/test_button.py +++ b/tests/components/opentherm_gw/test_button.py @@ -16,6 +16,40 @@ from .conftest import MINIMAL_STATUS from tests.common import MockConfigEntry +async def test_cancel_room_setpoint_override_button( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_pyotgw: MagicMock, +) -> None: + """Test cancel room setpoint override button.""" + + mock_pyotgw.return_value.set_target_temp = AsyncMock(return_value=0) + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + button_entity_id := entity_registry.async_get_entity_id( + BUTTON_DOMAIN, + OPENTHERM_DOMAIN, + f"{mock_config_entry.data[CONF_ID]}-{OpenThermDeviceIdentifier.THERMOSTAT}-cancel_room_setpoint_override", + ) + ) is not None + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: button_entity_id, + }, + blocking=True, + ) + + mock_pyotgw.return_value.set_target_temp.assert_awaited_once_with(0, True) + + async def test_restart_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py index dee16cd0632..c4cc830b89c 100644 --- a/tests/components/oralb/test_config_flow.py +++ b/tests/components/oralb/test_config_flow.py @@ -96,6 +96,36 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" +async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ORALB_SERVICE_INFO.address, + source=config_entries.SOURCE_IGNORE, + data={}, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.oralb.config_flow.async_discovered_service_info", + return_value=[ORALB_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "78:DB:2F:C2:48:BE"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Smart Series 7000 48BE" + assert result2["data"] == {} + assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/osoenergy/snapshots/test_water_heater.ambr b/tests/components/osoenergy/snapshots/test_water_heater.ambr index 5ebac405144..92b3a7aa099 100644 --- a/tests/components/osoenergy/snapshots/test_water_heater.ambr +++ b/tests/components/osoenergy/snapshots/test_water_heater.ambr @@ -9,6 +9,7 @@ 'min_temp': 10, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index 7d52318b477..5f778169e55 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -33,6 +33,8 @@ TEST_BORDER_AGENT_EXTENDED_ADDRESS = bytes.fromhex("AEEB2F594B570BBF") TEST_BORDER_AGENT_ID = bytes.fromhex("230C6A1AC57F6F4BE262ACF32E5EF52C") TEST_BORDER_AGENT_ID_2 = bytes.fromhex("230C6A1AC57F6F4BE262ACF32E5EF52D") +COPROCESSOR_VERSION = "OPENTHREAD/thread-reference-20200818-1740-g33cc75ed3; NRF52840; Jun 2 2022 14:25:49" + ROUTER_DISCOVERY_HASS = { "type_": "_meshcop._udp.local.", "name": "HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", @@ -60,3 +62,7 @@ ROUTER_DISCOVERY_HASS = { }, "interface_index": None, } + +TEST_COPROCESSOR_VERSION = ( + "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4; EFR32; Oct 21 2024 14:40:57" +) diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index 5ab3e442183..9140fcf6847 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -15,6 +15,7 @@ from . import ( DATASET_CH16, TEST_BORDER_AGENT_EXTENDED_ADDRESS, TEST_BORDER_AGENT_ID, + TEST_COPROCESSOR_VERSION, ) from tests.common import MockConfigEntry @@ -71,12 +72,23 @@ def get_extended_address_fixture() -> Generator[AsyncMock]: yield get_extended_address +@pytest.fixture(name="get_coprocessor_version") +def get_coprocessor_version_fixture() -> Generator[AsyncMock]: + """Mock get_coprocessor_version.""" + with patch( + "python_otbr_api.OTBR.get_coprocessor_version", + return_value=TEST_COPROCESSOR_VERSION, + ) as get_coprocessor_version: + yield get_coprocessor_version + + @pytest.fixture(name="otbr_config_entry_multipan") async def otbr_config_entry_multipan_fixture( hass: HomeAssistant, get_active_dataset_tlvs: AsyncMock, get_border_agent_id: AsyncMock, get_extended_address: AsyncMock, + get_coprocessor_version: AsyncMock, ) -> str: """Mock Open Thread Border Router config entry.""" config_entry = MockConfigEntry( @@ -97,6 +109,7 @@ async def otbr_config_entry_thread_fixture( get_active_dataset_tlvs: AsyncMock, get_border_agent_id: AsyncMock, get_extended_address: AsyncMock, + get_coprocessor_version: AsyncMock, ) -> None: """Mock Open Thread Border Router config entry.""" config_entry = MockConfigEntry( diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index cd02c14e4eb..8384b905b9c 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -10,9 +10,18 @@ import pytest import python_otbr_api from homeassistant.components import otbr +from homeassistant.components.homeassistant_hardware.helpers import ( + async_register_firmware_info_callback, +) +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, + OwningAddon, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo +from homeassistant.setup import async_setup_component from . import DATASET_CH15, DATASET_CH16, TEST_BORDER_AGENT_ID, TEST_BORDER_AGENT_ID_2 @@ -32,6 +41,19 @@ HASSIO_DATA_2 = HassioServiceInfo( uuid="23456", ) +HASSIO_DATA_OTBR = HassioServiceInfo( + config={ + "host": "core-openthread-border-router", + "port": 8081, + "device": "/dev/ttyUSB1", + "firmware": "SL-OPENTHREAD/2.4.4.0_GitHub-7074a43e4; EFR32; Oct 21 2024 14:40:57\r", + "addon": "OpenThread Border Router", + }, + name="OpenThread Border Router", + slug="core_openthread_border_router", + uuid="c58ba80fc88548008776bf8da903ef21", +) + @pytest.fixture(name="otbr_addon_info") def otbr_addon_info_fixture(addon_info: AsyncMock, addon_installed) -> AsyncMock: @@ -97,6 +119,7 @@ async def test_user_flow_additional_entry( @pytest.mark.usefixtures( "get_active_dataset_tlvs", "get_extended_address", + "get_coprocessor_version", ) async def test_user_flow_additional_entry_fail_get_address( hass: HomeAssistant, @@ -174,6 +197,7 @@ async def _finish_user_flow( "get_active_dataset_tlvs", "get_border_agent_id", "get_extended_address", + "get_coprocessor_version", ) async def test_user_flow_additional_entry_same_address( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker @@ -563,7 +587,11 @@ async def test_hassio_discovery_flow_2x_addons( assert config_entry.unique_id == HASSIO_DATA_2.uuid -@pytest.mark.usefixtures("get_active_dataset_tlvs", "get_extended_address") +@pytest.mark.usefixtures( + "get_active_dataset_tlvs", + "get_extended_address", + "get_coprocessor_version", +) async def test_hassio_discovery_flow_2x_addons_same_ext_address( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: @@ -830,7 +858,9 @@ async def test_hassio_discovery_flow_new_port_missing_unique_id( # Setup the config entry config_entry = MockConfigEntry( data={ - "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']+1}" + "url": ( + f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port'] + 1}" + ) }, domain=otbr.DOMAIN, options={}, @@ -861,7 +891,9 @@ async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None: # Setup the config entry config_entry = MockConfigEntry( data={ - "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']+1}" + "url": ( + f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port'] + 1}" + ) }, domain=otbr.DOMAIN, options={}, @@ -897,7 +929,9 @@ async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) - # Setup the config entry config_entry = MockConfigEntry( - data={"url": f"http://openthread_border_router:{HASSIO_DATA.config['port']+1}"}, + data={ + "url": f"http://openthread_border_router:{HASSIO_DATA.config['port'] + 1}" + }, domain=otbr.DOMAIN, options={}, source="hassio", @@ -914,7 +948,7 @@ async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) - # Make sure the data of the existing entry was not updated expected_data = { - "url": f"http://openthread_border_router:{HASSIO_DATA.config['port']+1}", + "url": f"http://openthread_border_router:{HASSIO_DATA.config['port'] + 1}", } config_entry = hass.config_entries.async_get_entry(config_entry.entry_id) assert config_entry.data == expected_data @@ -957,3 +991,55 @@ async def test_config_flow_additional_entry( ) assert result["type"] is expected_result + + +@pytest.mark.usefixtures( + "get_border_agent_id", "get_extended_address", "get_coprocessor_version" +) +async def test_hassio_discovery_reload( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info +) -> None: + """Test the hassio discovery flow.""" + await async_setup_component(hass, "homeassistant_hardware", {}) + + aioclient_mock.get( + "http://core-openthread-border-router:8081/node/dataset/active", text="" + ) + + callback = Mock() + async_register_firmware_info_callback(hass, "/dev/ttyUSB1", callback) + + with ( + patch( + "homeassistant.components.otbr.homeassistant_hardware.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.get_otbr_addon_firmware_info", + return_value=FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + OwningAddon(slug="core_openthread_border_router"), + ], + ), + ), + ): + await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_OTBR + ) + + # OTBR is set up and calls the firmware info notification callback + assert len(callback.mock_calls) == 1 + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 + + # If we change discovery info and emit again, the integration will be reloaded + # and firmware information will be broadcast again + await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA_OTBR + ) + + assert len(callback.mock_calls) == 2 + assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 diff --git a/tests/components/otbr/test_homeassistant_hardware.py b/tests/components/otbr/test_homeassistant_hardware.py new file mode 100644 index 00000000000..7f831656d06 --- /dev/null +++ b/tests/components/otbr/test_homeassistant_hardware.py @@ -0,0 +1,254 @@ +"""Test Home Assistant Hardware platform for OTBR.""" + +from unittest.mock import AsyncMock, Mock, call, patch + +import pytest + +from homeassistant.components.homeassistant_hardware.helpers import ( + async_register_firmware_info_callback, +) +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, + OwningAddon, + OwningIntegration, +) +from homeassistant.components.otbr.homeassistant_hardware import async_get_firmware_info +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from . import TEST_COPROCESSOR_VERSION + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +DEVICE_PATH = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9ab1da1ea4b3ed11956f4eaca7669f5d-if00-port0" + + +async def test_get_firmware_info(hass: HomeAssistant) -> None: + """Test `async_get_firmware_info`.""" + + otbr = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id", + data={ + "url": "http://core_openthread_border_router:8888", + }, + version=1, + ) + otbr.add_to_hass(hass) + otbr.mock_state(hass, ConfigEntryState.LOADED) + + otbr.runtime_data = AsyncMock() + otbr.runtime_data.get_coprocessor_version.return_value = TEST_COPROCESSOR_VERSION + + with ( + patch( + "homeassistant.components.otbr.homeassistant_hardware.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.AddonManager", + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.get_otbr_addon_firmware_info", + return_value=FirmwareInfo( + device=DEVICE_PATH, + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + OwningAddon(slug="core_openthread_border_router"), + ], + ), + ), + ): + fw_info = await async_get_firmware_info(hass, otbr) + + assert fw_info == FirmwareInfo( + device=DEVICE_PATH, + firmware_type=ApplicationType.SPINEL, + firmware_version=TEST_COPROCESSOR_VERSION, + source="otbr", + owners=[ + OwningIntegration(config_entry_id=otbr.entry_id), + OwningAddon(slug="core_openthread_border_router"), + ], + ) + + +async def test_get_firmware_info_ignored(hass: HomeAssistant) -> None: + """Test `async_get_firmware_info` with ignored entry.""" + + otbr = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id", + data={}, + version=1, + ) + otbr.add_to_hass(hass) + + fw_info = await async_get_firmware_info(hass, otbr) + assert fw_info is None + + +async def test_get_firmware_info_no_coprocessor_version(hass: HomeAssistant) -> None: + """Test `async_get_firmware_info` with no coprocessor version support.""" + + otbr = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id", + data={ + "url": "http://core_openthread_border_router:8888", + }, + version=1, + ) + otbr.add_to_hass(hass) + otbr.mock_state(hass, ConfigEntryState.LOADED) + + otbr.runtime_data = AsyncMock() + otbr.runtime_data.get_coprocessor_version.side_effect = HomeAssistantError() + + with ( + patch( + "homeassistant.components.otbr.homeassistant_hardware.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.AddonManager", + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.get_otbr_addon_firmware_info", + return_value=FirmwareInfo( + device=DEVICE_PATH, + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + OwningAddon(slug="core_openthread_border_router"), + ], + ), + ), + ): + fw_info = await async_get_firmware_info(hass, otbr) + + assert fw_info == FirmwareInfo( + device=DEVICE_PATH, + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + OwningIntegration(config_entry_id=otbr.entry_id), + OwningAddon(slug="core_openthread_border_router"), + ], + ) + + +@pytest.mark.parametrize( + ("version", "expected_version"), + [ + ((TEST_COPROCESSOR_VERSION,), TEST_COPROCESSOR_VERSION), + (HomeAssistantError(), None), + ], +) +async def test_hardware_firmware_info_provider_notification( + hass: HomeAssistant, + version: str | Exception, + expected_version: str | None, + get_active_dataset_tlvs: AsyncMock, + get_border_agent_id: AsyncMock, + get_extended_address: AsyncMock, + get_coprocessor_version: AsyncMock, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test that the OTBR provides hardware and firmware information.""" + otbr = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id", + data={ + "url": "http://core_openthread_border_router:8888", + }, + version=1, + ) + otbr.add_to_hass(hass) + + await async_setup_component(hass, "homeassistant_hardware", {}) + + callback = Mock() + async_register_firmware_info_callback(hass, DEVICE_PATH, callback) + + with ( + patch( + "homeassistant.components.otbr.homeassistant_hardware.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.AddonManager", + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.get_otbr_addon_firmware_info", + return_value=FirmwareInfo( + device=DEVICE_PATH, + firmware_type=ApplicationType.SPINEL, + firmware_version=None, + source="otbr", + owners=[ + OwningAddon(slug="core_openthread_border_router"), + ], + ), + ), + ): + get_coprocessor_version.side_effect = version + await hass.config_entries.async_setup(otbr.entry_id) + + assert callback.mock_calls == [ + call( + FirmwareInfo( + device=DEVICE_PATH, + firmware_type=ApplicationType.SPINEL, + firmware_version=expected_version, + source="otbr", + owners=[ + OwningIntegration(config_entry_id=otbr.entry_id), + OwningAddon(slug="core_openthread_border_router"), + ], + ) + ) + ] + + +async def test_get_firmware_info_remote_otbr(hass: HomeAssistant) -> None: + """Test `async_get_firmware_info` with no coprocessor version support.""" + + otbr = MockConfigEntry( + domain="otbr", + unique_id="some_unique_id", + data={ + "url": "http://192.168.1.10:8888", + }, + version=1, + ) + otbr.add_to_hass(hass) + otbr.mock_state(hass, ConfigEntryState.LOADED) + + otbr.runtime_data = AsyncMock() + otbr.runtime_data.get_coprocessor_version.return_value = TEST_COPROCESSOR_VERSION + + with ( + patch( + "homeassistant.components.otbr.homeassistant_hardware.is_hassio", + return_value=True, + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.AddonManager", + ), + patch( + "homeassistant.components.otbr.homeassistant_hardware.get_otbr_addon_firmware_info", + return_value=None, + ), + ): + fw_info = await async_get_firmware_info(hass, otbr) + + assert fw_info is None diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index faf13786107..b14527165e6 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -26,6 +26,7 @@ from . import ( ROUTER_DISCOVERY_HASS, TEST_BORDER_AGENT_EXTENDED_ADDRESS, TEST_BORDER_AGENT_ID, + TEST_COPROCESSOR_VERSION, ) from tests.common import MockConfigEntry @@ -43,6 +44,7 @@ def enable_mocks_fixture( get_active_dataset_tlvs: AsyncMock, get_border_agent_id: AsyncMock, get_extended_address: AsyncMock, + get_coprocessor_version: AsyncMock, ) -> None: """Enable API mocks.""" @@ -298,6 +300,7 @@ async def test_config_entry_update(hass: HomeAssistant) -> None: mock_api.get_extended_address = AsyncMock( return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS ) + mock_api.get_coprocessor_version = AsyncMock(return_value=TEST_COPROCESSOR_VERSION) with patch("python_otbr_api.OTBR", return_value=mock_api) as mock_otrb_api: assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/otp/conftest.py b/tests/components/otp/conftest.py index 7926be1e48e..d2a9e5c2b62 100644 --- a/tests/components/otp/conftest.py +++ b/tests/components/otp/conftest.py @@ -6,9 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.otp.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_TOKEN -from homeassistant.helpers.typing import ConfigType +from homeassistant.const import CONF_NAME, CONF_TOKEN from tests.common import MockConfigEntry @@ -51,15 +49,3 @@ def mock_otp_config_entry() -> MockConfigEntry: }, unique_id="2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", ) - - -@pytest.fixture(name="otp_yaml_config") -def mock_otp_yaml_config() -> ConfigType: - """Mock otp configuration entry.""" - return { - SENSOR_DOMAIN: { - CONF_PLATFORM: "otp", - CONF_TOKEN: "2FX5FBSYRE6VEC2FSHBQCRKO2GNDVZ52", - CONF_NAME: "OTP Sensor", - } - } diff --git a/tests/components/otp/test_config_flow.py b/tests/components/otp/test_config_flow.py index f9fac433ff9..f1e8428be44 100644 --- a/tests/components/otp/test_config_flow.py +++ b/tests/components/otp/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest from homeassistant.components.otp.const import CONF_NEW_TOKEN, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -97,22 +97,6 @@ async def test_errors_and_recover( assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.usefixtures("mock_pyotp", "mock_setup_entry") -async def test_flow_import(hass: HomeAssistant) -> None: - """Test that we can import a YAML config.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=TEST_DATA_RESULT, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "OTP Sensor" - assert result["data"] == TEST_DATA_RESULT - - @pytest.mark.usefixtures("mock_pyotp") async def test_generate_new_token( hass: HomeAssistant, mock_setup_entry: AsyncMock diff --git a/tests/components/otp/test_sensor.py b/tests/components/otp/test_sensor.py index e75ce6707d4..67894166e93 100644 --- a/tests/components/otp/test_sensor.py +++ b/tests/components/otp/test_sensor.py @@ -3,12 +3,7 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.otp.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -26,16 +21,3 @@ async def test_setup( await hass.async_block_till_done() assert hass.states.get("sensor.otp_sensor") == snapshot - - -async def test_deprecated_yaml_issue( - hass: HomeAssistant, issue_registry: ir.IssueRegistry, otp_yaml_config: ConfigType -) -> None: - """Test an issue is created when attempting setup from yaml config.""" - - assert await async_setup_component(hass, SENSOR_DOMAIN, otp_yaml_config) - await hass.async_block_till_done() - - assert issue_registry.async_get_issue( - domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}" - ) diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index cef5ef350a9..711cc6c1d86 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -17,11 +17,11 @@ from pyoverkiz.exceptions import ( import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.overkiz.const import DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -742,7 +742,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No """Test that DHCP discovery for new bridge works.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="gateway-1234-5678-9123", ip="192.168.1.4", macaddress="f8811a000000", @@ -801,7 +801,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="gateway-1234-5678-9123", ip="192.168.1.4", macaddress="f8811a000000", diff --git a/tests/components/overseerr/__init__.py b/tests/components/overseerr/__init__.py new file mode 100644 index 00000000000..b53b9d4f57a --- /dev/null +++ b/tests/components/overseerr/__init__.py @@ -0,0 +1,39 @@ +"""Tests for the Overseerr integration.""" + +from typing import Any +from urllib.parse import urlparse + +from aiohttp.test_utils import TestClient + +from homeassistant.components.webhook import async_generate_url +from homeassistant.core import HomeAssistant + +from .const import WEBHOOK_ID + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +async def call_webhook( + hass: HomeAssistant, data: dict[str, Any], client: TestClient +) -> None: + """Call the webhook.""" + webhook_url = async_generate_url(hass, WEBHOOK_ID) + + resp = await client.post( + urlparse(webhook_url).path, + json=data, + ) + + # Wait for remaining tasks to complete. + await hass.async_block_till_done() + + data = await resp.json() + resp.close() diff --git a/tests/components/overseerr/conftest.py b/tests/components/overseerr/conftest.py new file mode 100644 index 00000000000..9ae6be407ec --- /dev/null +++ b/tests/components/overseerr/conftest.py @@ -0,0 +1,120 @@ +"""Overseerr tests configuration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest +from python_overseerr import MovieDetails, RequestCount, RequestResponse +from python_overseerr.models import TVDetails, WebhookNotificationConfig + +from homeassistant.components.overseerr import CONF_CLOUDHOOK_URL +from homeassistant.components.overseerr.const import DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_WEBHOOK_ID, +) + +from .const import WEBHOOK_ID + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.overseerr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_overseerr_client() -> Generator[AsyncMock]: + """Mock an Overseerr client.""" + with ( + patch( + "homeassistant.components.overseerr.coordinator.OverseerrClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.overseerr.config_flow.OverseerrClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_request_count.return_value = RequestCount.from_json( + load_fixture("request_count.json", DOMAIN) + ) + client.get_webhook_notification_config.return_value = ( + WebhookNotificationConfig.from_json( + load_fixture("webhook_config.json", DOMAIN) + ) + ) + client.test_webhook_notification_config.return_value = True + client.get_requests.return_value = RequestResponse.from_json( + load_fixture("requests.json", DOMAIN) + ).results + client.get_movie_details.return_value = MovieDetails.from_json( + load_fixture("movie.json", DOMAIN) + ) + client.get_tv_details.return_value = TVDetails.from_json( + load_fixture("tv.json", DOMAIN) + ) + yield client + + +@pytest.fixture +def mock_overseerr_client_needs_change( + mock_overseerr_client: AsyncMock, +) -> Generator[AsyncMock]: + """Mock an Overseerr client.""" + mock_overseerr_client.get_webhook_notification_config.return_value.types = 0 + return mock_overseerr_client + + +@pytest.fixture +def mock_overseerr_client_cloudhook( + mock_overseerr_client: AsyncMock, +) -> Generator[AsyncMock]: + """Mock an Overseerr client.""" + mock_overseerr_client.get_webhook_notification_config.return_value.options.webhook_url = "https://hooks.nabu.casa/ABCD" + return mock_overseerr_client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Overseerr", + data={ + CONF_HOST: "overseerr.test", + CONF_PORT: 80, + CONF_SSL: False, + CONF_API_KEY: "test-key", + CONF_WEBHOOK_ID: WEBHOOK_ID, + }, + entry_id="01JG00V55WEVTJ0CJHM0GAD7PC", + ) + + +@pytest.fixture +def mock_cloudhook_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Overseerr", + data={ + CONF_HOST: "overseerr.test", + CONF_PORT: 80, + CONF_SSL: False, + CONF_API_KEY: "test-key", + CONF_WEBHOOK_ID: WEBHOOK_ID, + CONF_CLOUDHOOK_URL: "https://hooks.nabu.casa/ABCD", + }, + entry_id="01JG00V55WEVTJ0CJHM0GAD7PC", + ) diff --git a/tests/components/overseerr/const.py b/tests/components/overseerr/const.py new file mode 100644 index 00000000000..8c5c86e3ee4 --- /dev/null +++ b/tests/components/overseerr/const.py @@ -0,0 +1,3 @@ +"""Constants for the Overseerr tests.""" + +WEBHOOK_ID = "test-webhook-id" diff --git a/tests/components/overseerr/fixtures/movie.json b/tests/components/overseerr/fixtures/movie.json new file mode 100644 index 00000000000..2f5e86b3261 --- /dev/null +++ b/tests/components/overseerr/fixtures/movie.json @@ -0,0 +1,2644 @@ +{ + "id": 1156593, + "adult": false, + "budget": 0, + "genres": [ + { + "id": 10749, + "name": "Romance" + }, + { + "id": 18, + "name": "Drama" + } + ], + "relatedVideos": [ + { + "site": "YouTube", + "key": "IoyJOL5WnvQ", + "name": "Nick and Noah Meet Briar and Sofia [Subtitled]", + "size": 1080, + "type": "Clip", + "url": "https://www.youtube.com/watch?v=IoyJOL5WnvQ" + }, + { + "site": "YouTube", + "key": "nuPjieeswM4", + "name": "Sneak Peek: Nick and Noah are Back [Subtitled]", + "size": 1080, + "type": "Clip", + "url": "https://www.youtube.com/watch?v=nuPjieeswM4" + }, + { + "site": "YouTube", + "key": "QT5j0Uf-rdM", + "name": "Official Trailer 2 [Subtitled]", + "size": 1080, + "type": "Trailer", + "url": "https://www.youtube.com/watch?v=QT5j0Uf-rdM" + }, + { + "site": "YouTube", + "key": "m_TWESxP_DE", + "name": "Official Trailer [Subtitled]", + "size": 1080, + "type": "Trailer", + "url": "https://www.youtube.com/watch?v=m_TWESxP_DE" + }, + { + "site": "YouTube", + "key": "Wp4S0-fWgI0", + "name": "Official Teaser [Subtitled]", + "size": 1080, + "type": "Teaser", + "url": "https://www.youtube.com/watch?v=Wp4S0-fWgI0" + }, + { + "site": "YouTube", + "key": "3KTZekgTyFU", + "name": "Date Announce", + "size": 1080, + "type": "Teaser", + "url": "https://www.youtube.com/watch?v=3KTZekgTyFU" + } + ], + "originalLanguage": "es", + "originalTitle": "Culpa tuya", + "popularity": 3958.479, + "productionCompanies": [ + { + "id": 32485, + "name": "Pokeepsie Films", + "originCountry": "ES", + "logoPath": "/fVXreZkiJTOq1cCi6htSQlPSy7R.png" + }, + { + "id": 210099, + "name": "Amazon MGM Studios", + "originCountry": "US", + "logoPath": "/d6HwljzlOzxJ4tXlrpRkNZaZMWL.png" + } + ], + "productionCountries": [ + { + "iso_3166_1": "ES", + "name": "Spain" + }, + { + "iso_3166_1": "US", + "name": "United States of America" + } + ], + "releaseDate": "2024-12-26", + "releases": { + "results": [ + { + "iso_3166_1": "AR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "AS", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-26T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "AT", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "BE", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "BG", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "BO", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "BR", + "release_dates": [ + { + "certification": "18", + "descriptors": ["Sexual Content", "Illegal Drugs"], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-26T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "CA", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "CH", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "CL", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "CO", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "CR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "CZ", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "DE", + "release_dates": [ + { + "certification": "16", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "DK", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "EC", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "EG", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "ES", + "release_dates": [ + { + "certification": "16", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "FI", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "FR", + "release_dates": [ + { + "certification": "16", + "descriptors": [], + "iso_639_1": "", + "note": "Prime vidéo", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "GB", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "GR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "GT", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "GU", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "HK", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "HR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "HU", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "ID", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "IE", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "IN", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "IT", + "release_dates": [ + { + "certification": "14+", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "JP", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "KR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "MP", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "MX", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "MY", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "NL", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "NO", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "NZ", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "PE", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "PH", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "PK", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "PL", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "PR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-26T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "PT", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "RO", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "SA", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "SE", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "SG", + "release_dates": [ + { + "certification": "M18", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "SK", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "TH", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "TR", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "TW", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "US", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-26T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "VE", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "VI", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-26T00:00:00.000Z", + "type": 4 + } + ] + }, + { + "iso_3166_1": "ZA", + "release_dates": [ + { + "certification": "", + "descriptors": [], + "iso_639_1": "", + "note": "Prime Video", + "release_date": "2024-12-27T00:00:00.000Z", + "type": 4 + } + ] + } + ] + }, + "revenue": 0, + "spokenLanguages": [ + { + "english_name": "Spanish", + "iso_639_1": "es", + "name": "Español" + } + ], + "status": "Released", + "title": "Your Fault", + "video": false, + "voteAverage": 7.7, + "voteCount": 190, + "backdropPath": "/6qld2YxAO9gdEblo0rsEb8BcYKO.jpg", + "homepage": "https://www.amazon.com/dp/B0DJ1L7ZN3", + "imdbId": "tt28510079", + "overview": "The love between Noah and Nick seems unwavering despite their parents' attempts to separate them. But his job and her entry into college open up their lives to new relationships that will shake the foundations of both their relationship and the Leister family itself.", + "posterPath": "/1sQA7lfcF9yUyoLYC0e6Zo3jmxE.jpg", + "runtime": 120, + "tagline": "Divided by family. Driven by love.", + "credits": { + "cast": [ + { + "castId": 1, + "character": "Noah Morgan", + "creditId": "64c12ecefdc1460139f9e120", + "id": 2527414, + "name": "Nicole Wallace", + "order": 0, + "gender": 1, + "profilePath": "/gSGCoJVKAMUTmyA3GzCAX22WNBA.jpg" + }, + { + "castId": 2, + "character": "Nick Leister", + "creditId": "64c12ed6df86a80125827557", + "id": 2786960, + "name": "Gabriel Guevara", + "order": 1, + "gender": 2, + "profilePath": "/pviRYKEEmoPUfLYwP1VHJ6LQcRg.jpg" + }, + { + "castId": 12, + "character": "William Leister", + "creditId": "65e0ed456a300b017d1dc9fc", + "id": 1251336, + "name": "Iván Sánchez", + "order": 2, + "gender": 2, + "profilePath": "/woVz8D7t1VUKjFJnsTAdc8tyz5C.jpg" + }, + { + "castId": 13, + "character": "Rafaela", + "creditId": "65e0ed9851f99a018557897f", + "id": 970027, + "name": "Marta Hazas", + "order": 3, + "gender": 1, + "profilePath": "/1dbeTFRCbWBt70dIGjYHKVLnpaG.jpg" + }, + { + "castId": 14, + "character": "Jenna", + "creditId": "65e0edda41a561016382560b", + "id": 4105602, + "name": "Eva Ruiz", + "order": 4, + "gender": 1, + "profilePath": "/bcu0nmQvhxwTzh4csc4kuxJsQee.jpg" + }, + { + "castId": 15, + "character": "Lion", + "creditId": "65e0ef982d531a0162bf8423", + "id": 2126335, + "name": "Víctor Varona", + "order": 5, + "gender": 0, + "profilePath": "/lcwFAjHjhJXkxf59TXGSjGGOlLj.jpg" + }, + { + "castId": 16, + "character": "Sofía", + "creditId": "65e0f001724de10187de9bf1", + "id": 3513082, + "name": "Gabriela Andrada", + "order": 6, + "gender": 1, + "profilePath": "/h8cAdDqbDK2ayGIuxQQdNCzyCsb.jpg" + }, + { + "castId": 17, + "character": "Briar", + "creditId": "65e0f09541a5610186825d32", + "id": 3904252, + "name": "Alex Bejar", + "order": 7, + "gender": 1, + "profilePath": "/kPeFa6xX57IyhAfoT8dtgfPMgmX.jpg" + }, + { + "castId": 18, + "character": "Michael", + "creditId": "65e0f0b46a300b017d1dcbde", + "id": 2785763, + "name": "Javier Morgade", + "order": 8, + "gender": 2, + "profilePath": "/hcDV10bTZlm1rTi0NRhoGIpdicw.jpg" + }, + { + "castId": 19, + "character": "Luca", + "creditId": "65e0f35ea39d0b018608bda5", + "id": 3117422, + "name": "Felipe Londoño", + "order": 9, + "gender": 0, + "profilePath": "/gUKu2bla3ZKtwuzfymMbyzUsobC.jpg" + }, + { + "castId": 20, + "character": "Anabel", + "creditId": "65e0f37c9dee58017c7cdb1f", + "id": 261, + "name": "Goya Toledo", + "order": 10, + "gender": 1, + "profilePath": "/dihdQxVr2sFA1XznPK4orzC2m7i.jpg" + }, + { + "castId": 21, + "character": "Simón", + "creditId": "65e0f3b107e2810163dc6d82", + "id": 1983424, + "name": "Fran Morcillo", + "order": 11, + "gender": 2, + "profilePath": "/ovPzGuRvMQhB4OrC6lfxlEGLewD.jpg" + }, + { + "castId": 22, + "character": "Dioni", + "creditId": "65e0f420724de10187de9dbf", + "id": 2508286, + "name": "José Manuel Palacios", + "order": 12, + "gender": 2, + "profilePath": "/1oXz8Hht5pBluq700BmHBQwWbVQ.jpg" + } + ], + "crew": [ + { + "creditId": "64c54c10cadb6b01440d5d47", + "department": "Writing", + "id": 4106905, + "job": "Novel", + "name": "Mercedes Ron", + "gender": 1, + "profilePath": null + }, + { + "creditId": "64c12f562f1be000ae4ba431", + "department": "Production", + "id": 57865, + "job": "Producer", + "name": "Álex de la Iglesia", + "gender": 2, + "profilePath": "/vyZFSSNtY2jL06v4nJHLIT5fWa2.jpg" + }, + { + "creditId": "64c12f63097c49011d82bc77", + "department": "Production", + "id": 512958, + "job": "Producer", + "name": "Carolina Bang", + "gender": 1, + "profilePath": "/zwY5dMMeRAxUGU2Q50u7mWk3DA0.jpg" + }, + { + "creditId": "64c17c3d871b34011e697f24", + "department": "Directing", + "id": 1739102, + "job": "Director", + "name": "Domingo González", + "gender": 0, + "profilePath": null + }, + { + "creditId": "674ddb345c9ffdbae7692b9a", + "department": "Writing", + "id": 1739102, + "job": "Screenplay", + "name": "Domingo González", + "gender": 0, + "profilePath": null + }, + { + "creditId": "674ddb3f7473778bbd9afc75", + "department": "Writing", + "id": 1495797, + "job": "Screenplay", + "name": "Sofía Cuenca", + "gender": 0, + "profilePath": "/d5JbOSgQFcLd0GlBSotuNXK9Yia.jpg" + } + ] + }, + "collection": { + "id": 1156666, + "name": "Fault Collection", + "posterPath": "/cAimhhErEnKdvaZoU6Wtbf38sTS.jpg", + "backdropPath": "/7N9p8D39MXJDfm14zlBwW7mBh7g.jpg" + }, + "externalIds": { + "facebookId": null, + "imdbId": "tt28510079", + "instagramId": null, + "twitterId": null + }, + "mediaInfo": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 537, + "mediaType": "movie", + "tmdbId": 1156593, + "tvdbId": null, + "imdbId": null, + "status": 3, + "status4k": 1, + "createdAt": "2024-12-29T10:04:16.000Z", + "updatedAt": "2024-12-29T10:04:17.000Z", + "lastSeasonChange": "2024-12-29T10:04:16.000Z", + "mediaAddedAt": null, + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 423, + "externalServiceId4k": null, + "externalServiceSlug": "1156593", + "externalServiceSlug4k": null, + "ratingKey": null, + "ratingKey4k": null, + "requests": [ + { + "id": 16, + "status": 2, + "createdAt": "2024-12-29T10:04:16.000Z", + "updatedAt": "2024-12-29T10:04:16.000Z", + "type": "movie", + "is4k": false, + "serverId": 0, + "profileId": 7, + "rootFolder": "/media/movies", + "languageProfileId": null, + "tags": [], + "isAutoRequest": false, + "media": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 537, + "mediaType": "movie", + "tmdbId": 1156593, + "tvdbId": null, + "imdbId": null, + "status": 3, + "status4k": 1, + "createdAt": "2024-12-29T10:04:16.000Z", + "updatedAt": "2024-12-29T10:04:17.000Z", + "lastSeasonChange": "2024-12-29T10:04:16.000Z", + "mediaAddedAt": null, + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 423, + "externalServiceId4k": null, + "externalServiceSlug": "1156593", + "externalServiceSlug4k": null, + "ratingKey": null, + "ratingKey4k": null, + "serviceUrl": "http://192.168.0.1:7878/movie/1156593" + }, + "requestedBy": { + "permissions": 2, + "id": 1, + "email": "me@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 43972689, + "avatar": "https://plex.tv/users/aaaaaaa/avatar?c=aaaaaaa", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "modifiedBy": { + "permissions": 2, + "id": 1, + "email": "me@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 43972689, + "avatar": "https://plex.tv/users/aaaaaaa/avatar?c=aaaaaaa", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "seasons": [], + "seasonCount": 0 + } + ], + "issues": [], + "seasons": [], + "serviceUrl": "http://192.168.0.1:7878/movie/1156593" + }, + "watchProviders": [ + { + "iso_3166_1": "AD", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AD", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AE", + "buy": [], + "flatrate": [ + { + "displayPriority": 9, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AL", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AL", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AR", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 65, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "AU", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=AU", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BB", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BB", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BE", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BH", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BM", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BO", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BR", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BS", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=BS", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CA", + "buy": [], + "flatrate": [ + { + "displayPriority": 3, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 152, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "CH", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CI", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CL", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CL", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CO", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CZ", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=CZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "DE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=DE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 157, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "DK", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=DK", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "DO", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=DO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "DZ", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=DZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "EC", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=EC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "EE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=EE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "EG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=EG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ES", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=ES", + "buy": [], + "flatrate": [ + { + "displayPriority": 3, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "FI", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=FI", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "FR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=FR", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GB", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GB", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 136, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "GF", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GF", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GH", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GI", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=GT", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HK", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=HK", + "buy": [], + "flatrate": [ + { + "displayPriority": 5, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HN", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=HN", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=HR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HU", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=HU", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ID", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=ID", + "buy": [], + "flatrate": [ + { + "displayPriority": 7, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=IE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IL", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=IL", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IN", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=IN", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IQ", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=IQ", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IS", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=IS", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=IT", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 15, + "logoPath": "/6FDKQWcR6JfmRKLqezSsvGgRuUY.jpg", + "id": 109, + "name": "Timvision" + } + ] + }, + { + "iso_3166_1": "JM", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=JM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "JO", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=JO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "JP", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=JP", + "buy": [], + "flatrate": [ + { + "displayPriority": 4, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "KE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=KE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "KR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=KR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "KW", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=KW", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LB", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=LB", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LC", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=LC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LI", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=LI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=LT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LV", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=LV", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LY", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=LY", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MC", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MD", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MD", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MK", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MK", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MX", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MX", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MY", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MY", + "buy": [], + "flatrate": [ + { + "displayPriority": 9, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MZ", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=MZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=NE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=NG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NL", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=NL", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NO", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=NO", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NZ", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=NZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "OM", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=OM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PF", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PF", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PH", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PK", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PK", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PL", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PL", + "buy": [], + "flatrate": [ + { + "displayPriority": 5, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PS", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PS", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PT", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PY", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=PY", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "QA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=QA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "RO", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=RO", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "RS", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=RS", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SA", + "buy": [], + "flatrate": [ + { + "displayPriority": 23, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SE", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SI", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SK", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SK", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SM", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SV", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=SV", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TC", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=TC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TH", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=TH", + "buy": [], + "flatrate": [ + { + "displayPriority": 3, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TN", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=TN", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TR", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=TR", + "buy": [], + "flatrate": [ + { + "displayPriority": 6, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TT", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=TT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TW", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=TW", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "UG", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=UG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "US", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=US", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 258, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "UY", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=UY", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "VA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=VA", + "buy": [], + "flatrate": [ + { + "displayPriority": 0, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "VE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=VE", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "YE", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=YE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ZA", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=ZA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ZM", + "link": "https://www.themoviedb.org/movie/1156593-culpa-tuya/watch?locale=ZM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + } + ], + "keywords": [ + { + "id": 818, + "name": "based on novel or book" + }, + { + "id": 9663, + "name": "sequel" + } + ] +} diff --git a/tests/components/overseerr/fixtures/request_count.json b/tests/components/overseerr/fixtures/request_count.json new file mode 100644 index 00000000000..441429cfa00 --- /dev/null +++ b/tests/components/overseerr/fixtures/request_count.json @@ -0,0 +1,10 @@ +{ + "total": 11, + "movie": 9, + "tv": 2, + "pending": 0, + "approved": 11, + "declined": 0, + "processing": 3, + "available": 8 +} diff --git a/tests/components/overseerr/fixtures/requests.json b/tests/components/overseerr/fixtures/requests.json new file mode 100644 index 00000000000..ff422420893 --- /dev/null +++ b/tests/components/overseerr/fixtures/requests.json @@ -0,0 +1,176 @@ +{ + "pageInfo": { + "pages": 2, + "pageSize": 10, + "results": 14, + "page": 1 + }, + "results": [ + { + "id": 16, + "status": 2, + "createdAt": "2024-12-29T10:04:16.000Z", + "updatedAt": "2024-12-29T10:04:16.000Z", + "type": "movie", + "is4k": false, + "serverId": 0, + "profileId": 7, + "rootFolder": "/media/movies", + "languageProfileId": null, + "tags": [], + "isAutoRequest": false, + "media": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 537, + "mediaType": "movie", + "tmdbId": 1156593, + "tvdbId": null, + "imdbId": null, + "status": 3, + "status4k": 1, + "createdAt": "2024-12-29T10:04:16.000Z", + "updatedAt": "2024-12-29T10:04:17.000Z", + "lastSeasonChange": "2024-12-29T10:04:16.000Z", + "mediaAddedAt": null, + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 423, + "externalServiceId4k": null, + "externalServiceSlug": "1156593", + "externalServiceSlug4k": null, + "ratingKey": null, + "ratingKey4k": null, + "serviceUrl": "http://192.168.0.1:7878/movie/1156593" + }, + "seasons": [], + "modifiedBy": { + "permissions": 2, + "id": 1, + "email": "one@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 321321321, + "avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "requestedBy": { + "permissions": 2, + "id": 1, + "email": "one@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 321321321, + "avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "seasonCount": 0 + }, + { + "id": 14, + "status": 2, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:37:30.000Z", + "type": "tv", + "is4k": false, + "serverId": 0, + "profileId": 7, + "rootFolder": "/media/tv", + "languageProfileId": 1, + "tags": [], + "isAutoRequest": false, + "media": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 535, + "mediaType": "tv", + "tmdbId": 249522, + "tvdbId": 447806, + "imdbId": null, + "status": 4, + "status4k": 1, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:45:00.000Z", + "lastSeasonChange": "2024-12-26T14:37:30.000Z", + "mediaAddedAt": "2024-12-26T14:39:56.000Z", + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 144, + "externalServiceId4k": null, + "externalServiceSlug": "beast-games", + "externalServiceSlug4k": null, + "ratingKey": "10189", + "ratingKey4k": null, + "plexUrl": "https://app.plex.tv/desktop#!/server/aaaa/details?key=%2Flibrary%2Fmetadata%2F10189", + "iOSPlexUrl": "plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F10189&server=aaaa", + "serviceUrl": "http://192.168.0.2:8989/series/beast-games" + }, + "seasons": [ + { + "id": 4, + "seasonNumber": 1, + "status": 2, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:37:30.000Z" + } + ], + "modifiedBy": { + "permissions": 2, + "id": 1, + "email": "one@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 321321321, + "avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "requestedBy": { + "permissions": 2, + "id": 1, + "email": "one@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 321321321, + "avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "seasonCount": 1 + } + ] +} diff --git a/tests/components/overseerr/fixtures/tv.json b/tests/components/overseerr/fixtures/tv.json new file mode 100644 index 00000000000..445197de0ad --- /dev/null +++ b/tests/components/overseerr/fixtures/tv.json @@ -0,0 +1,2052 @@ +{ + "createdBy": [ + { + "id": 2282154, + "credit_id": "673fc8e2d7bee5585c559789", + "name": "Jimmy Donaldson", + "original_name": "Jimmy Donaldson", + "gender": 2, + "profile_path": "/dE6VIMrvZsY5a9gIaWoX40tzYkV.jpg" + }, + { + "id": 4886515, + "credit_id": "673fc8ecd7bee5585c55979f", + "name": "Tyler Conklin", + "original_name": "Tyler Conklin", + "gender": 2, + "profile_path": null + }, + { + "id": 572350, + "credit_id": "673fc8f4d7bee5585c5597a7", + "name": "Sean Klitzner", + "original_name": "Sean Klitzner", + "gender": 2, + "profile_path": "/rTVGa5hK5N9mhJwrd2oGOWzbeh7.jpg" + }, + { + "id": 5073789, + "credit_id": "673fc8f88337acae076dca7d", + "name": "Mack Hopkins", + "original_name": "Mack Hopkins", + "gender": 2, + "profile_path": "/5yaA2crWsdzTf2BFnlZ3ZuvFaL8.jpg" + } + ], + "episodeRunTime": [], + "firstAirDate": "2024-12-19", + "genres": [ + { + "id": 10764, + "name": "Reality" + } + ], + "relatedVideos": [ + { + "site": "YouTube", + "key": "37ZG9lddxto", + "name": "Official Trailer", + "size": 1080, + "type": "Trailer", + "url": "https://www.youtube.com/watch?v=37ZG9lddxto" + }, + { + "site": "YouTube", + "key": "2lyCZnx71ps", + "name": "Teaser Trailer", + "size": 1080, + "type": "Teaser", + "url": "https://www.youtube.com/watch?v=2lyCZnx71ps" + } + ], + "homepage": "https://www.amazon.com/dp/B0CZ7S3N9Y", + "id": 249522, + "inProduction": true, + "languages": ["da", "en"], + "lastAirDate": "2024-12-26", + "name": "Beast Games", + "networks": [ + { + "id": 1024, + "name": "Prime Video", + "originCountry": "", + "logoPath": "/ifhbNuuVnlwYy5oXA5VIb2YR8AZ.png" + } + ], + "numberOfEpisodes": 10, + "numberOfSeasons": 1, + "originCountry": ["US"], + "originalLanguage": "en", + "originalName": "Beast Games", + "tagline": "1,000 players. 5 million dollars. 1 winner.", + "overview": "I gathered 1,000 people to fight for $5,000,000, the LARGEST cash prize in TV history! We're also giving away a private island, Lamborghinis, and millions more in cash throughout the competition! Go watch to see the greatest show ever made!", + "popularity": 769.189, + "productionCompanies": [ + { + "id": 210099, + "name": "Amazon MGM Studios", + "originCountry": "US", + "logoPath": "/d6HwljzlOzxJ4tXlrpRkNZaZMWL.png" + }, + { + "id": 247865, + "name": "MrBeast", + "originCountry": "US", + "logoPath": "/9jchzJF1IonYRakcyVIu2fKb8Mz.png" + } + ], + "productionCountries": [ + { + "iso_3166_1": "US", + "name": "United States of America" + } + ], + "contentRatings": { + "results": [ + { + "descriptors": [], + "iso_3166_1": "SG", + "rating": "G" + }, + { + "descriptors": [], + "iso_3166_1": "BR", + "rating": "L" + }, + { + "descriptors": [], + "iso_3166_1": "MX", + "rating": "A" + }, + { + "descriptors": [], + "iso_3166_1": "US", + "rating": "TV-G" + }, + { + "descriptors": [], + "iso_3166_1": "ES", + "rating": "7" + } + ] + }, + "spokenLanguages": [ + { + "englishName": "Danish", + "iso_639_1": "da", + "name": "Dansk" + }, + { + "englishName": "English", + "iso_639_1": "en", + "name": "English" + } + ], + "seasons": [ + { + "airDate": "2024-12-19", + "episodeCount": 10, + "id": 384427, + "name": "Season 1", + "overview": "", + "seasonNumber": 1, + "posterPath": "/3itZlypnOcVcqI5xxyO6nvJ52yM.jpg" + } + ], + "status": "Returning Series", + "type": "Reality", + "voteAverage": 7.523, + "voteCount": 44, + "backdropPath": "/bXJII8e2iIQ7EzDx4KRhSghn2me.jpg", + "lastEpisodeToAir": { + "id": 5802152, + "airDate": "2024-12-26", + "episodeNumber": 3, + "name": "The Solitary Experiment", + "overview": "What would happen if three best friends were trapped in a room, but only two could escape? Watch and see for yourself right now!", + "productionCode": "", + "seasonNumber": 1, + "showId": 249522, + "voteAverage": 4.6, + "stillPath": "/r6LRRaA2l2tMDttWbYl3dXdJUij.jpg" + }, + "nextEpisodeToAir": { + "id": 5802153, + "airDate": "2025-01-02", + "episodeNumber": 4, + "name": "Episode 4", + "overview": "", + "productionCode": "", + "seasonNumber": 1, + "showId": 249522, + "voteAverage": 0, + "stillPath": null + }, + "posterPath": "/3itZlypnOcVcqI5xxyO6nvJ52yM.jpg", + "credits": { + "cast": [ + { + "character": "Self - Host", + "creditId": "65f9449f15a4a1012c0c8d19", + "id": 2282154, + "name": "Jimmy Donaldson", + "order": 0, + "gender": 2, + "profilePath": "/dE6VIMrvZsY5a9gIaWoX40tzYkV.jpg" + }, + { + "character": "Self - Co-Host", + "creditId": "6765bca2330bce6ec990e041", + "id": 5131521, + "name": "Chandler Hallow", + "order": 1, + "gender": 2, + "profilePath": "/8MAvWDC4FiiSYlmpZ9p2oCovxl0.jpg" + }, + { + "character": "Self - Co-Host", + "creditId": "6765bcb9330bce6ec990e059", + "id": 5131522, + "name": "Karl Jacobs", + "order": 2, + "gender": 2, + "profilePath": "/9ZNCxiXh4l3i24KXLD67Lqyv04K.jpg" + }, + { + "character": "Self - Co-Host", + "creditId": "6765bcbf87b92a43be5e00bd", + "id": 5131524, + "name": "Nolan Hansen", + "order": 3, + "gender": 2, + "profilePath": "/uyEUcMieLKH6pn9JTPuoI5nURWB.jpg" + }, + { + "character": "Self - Co-Host", + "creditId": "6765bcca919287ef5390dc73", + "id": 5131525, + "name": "Tareq Salameh", + "order": 4, + "gender": 2, + "profilePath": "/bFZn6A8CSn7EPi9MHcIYxTpLAlF.jpg" + }, + { + "character": "Self - Co-Host", + "creditId": "6765bcd1919287ef5390dc7c", + "id": 5073789, + "name": "Mack Hopkins", + "order": 5, + "gender": 2, + "profilePath": "/5yaA2crWsdzTf2BFnlZ3ZuvFaL8.jpg" + }, + { + "character": "Self", + "creditId": "6767dae0ae1b411f405e4af9", + "id": 1126417, + "name": "Arturo del Puerto", + "order": 6, + "gender": 2, + "profilePath": "/tALGmngyUUtxyeQtMSy7KyzjdYc.jpg" + } + ], + "crew": [ + { + "creditId": "65f94625223e20016079bd7c", + "department": "Production", + "id": 2282154, + "job": "Executive Producer", + "name": "Jimmy Donaldson", + "gender": 2, + "profilePath": "/dE6VIMrvZsY5a9gIaWoX40tzYkV.jpg" + }, + { + "creditId": "673fcb2b7b825e685b4e0a34", + "department": "Production", + "id": 4886515, + "job": "Executive Producer", + "name": "Tyler Conklin", + "gender": 2, + "profilePath": null + }, + { + "creditId": "673fcb357b825e685b4e0a37", + "department": "Production", + "id": 572350, + "job": "Executive Producer", + "name": "Sean Klitzner", + "gender": 2, + "profilePath": "/rTVGa5hK5N9mhJwrd2oGOWzbeh7.jpg" + }, + { + "creditId": "673fcb41dae2e6a938254d38", + "department": "Production", + "id": 5073789, + "job": "Executive Producer", + "name": "Mack Hopkins", + "gender": 2, + "profilePath": "/5yaA2crWsdzTf2BFnlZ3ZuvFaL8.jpg" + }, + { + "creditId": "673fcb57dae2e6a938254d4e", + "department": "Production", + "id": 5073813, + "job": "Executive Producer", + "name": "Michael Cruz", + "gender": 0, + "profilePath": null + }, + { + "creditId": "673fcb605cac041cfae26a54", + "department": "Production", + "id": 4851894, + "job": "Executive Producer", + "name": "Matt Apps", + "gender": 0, + "profilePath": null + }, + { + "creditId": "673fcb6b7b825e685b4e0a3d", + "department": "Production", + "id": 4007900, + "job": "Executive Producer", + "name": "Charles Wachter", + "gender": 0, + "profilePath": null + }, + { + "creditId": "673fcb747b825e685b4e0a43", + "department": "Production", + "id": 2292335, + "job": "Executive Producer", + "name": "Keith Geller", + "gender": 0, + "profilePath": null + }, + { + "creditId": "673fcb807b825e685b4e0a4d", + "department": "Production", + "id": 1251868, + "job": "Executive Producer", + "name": "Joe Coleman", + "gender": 2, + "profilePath": null + }, + { + "creditId": "673fcb877b825e685b4e0a53", + "department": "Production", + "id": 1450643, + "job": "Executive Producer", + "name": "Rachel Skidmore", + "gender": 0, + "profilePath": null + }, + { + "creditId": "673fcb91dae2e6a938254d5c", + "department": "Production", + "id": 4338334, + "job": "Executive Producer", + "name": "Chris Keiper", + "gender": 0, + "profilePath": null + }, + { + "creditId": "673fcb9a5cac041cfae26a5c", + "department": "Production", + "id": 5073815, + "job": "Executive Producer", + "name": "Joshua Kulic", + "gender": 0, + "profilePath": null + } + ] + }, + "externalIds": { + "facebookId": null, + "freebaseId": null, + "freebaseMid": null, + "imdbId": "tt31812476", + "instagramId": "beastgamesonprime", + "tvdbId": 447806, + "tvrageId": null, + "twitterId": null + }, + "keywords": [ + { + "id": 271, + "name": "competition" + }, + { + "id": 4325, + "name": "game show" + }, + { + "id": 330122, + "name": "mrbeast" + } + ], + "mediaInfo": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 535, + "mediaType": "tv", + "tmdbId": 249522, + "tvdbId": 447806, + "imdbId": null, + "status": 4, + "status4k": 1, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:45:00.000Z", + "lastSeasonChange": "2024-12-26T14:37:30.000Z", + "mediaAddedAt": "2024-12-26T14:39:56.000Z", + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 144, + "externalServiceId4k": null, + "externalServiceSlug": "beast-games", + "externalServiceSlug4k": null, + "ratingKey": "10189", + "ratingKey4k": null, + "requests": [ + { + "id": 14, + "status": 2, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:37:30.000Z", + "type": "tv", + "is4k": false, + "serverId": 0, + "profileId": 7, + "rootFolder": "/media/tv", + "languageProfileId": 1, + "tags": [], + "isAutoRequest": false, + "media": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 535, + "mediaType": "tv", + "tmdbId": 249522, + "tvdbId": 447806, + "imdbId": null, + "status": 4, + "status4k": 1, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:45:00.000Z", + "lastSeasonChange": "2024-12-26T14:37:30.000Z", + "mediaAddedAt": "2024-12-26T14:39:56.000Z", + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 144, + "externalServiceId4k": null, + "externalServiceSlug": "beast-games", + "externalServiceSlug4k": null, + "ratingKey": "10189", + "ratingKey4k": null, + "plexUrl": "https://app.plex.tv/desktop#!/server/aaaaaaa/details?key=%2Flibrary%2Fmetadata%2F10189", + "iOSPlexUrl": "plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F10189&server=aaaaaaa", + "serviceUrl": "http://192.168.0.2:8989/series/beast-games" + }, + "requestedBy": { + "permissions": 2, + "id": 1, + "email": "me@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 123123, + "avatar": "https://plex.tv/users/123123/avatar?c=123123", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "modifiedBy": { + "permissions": 2, + "id": 1, + "email": "me@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 123123, + "avatar": "https://plex.tv/users/123123/avatar?c=123123", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "seasons": [ + { + "id": 4, + "seasonNumber": 1, + "status": 2, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:37:30.000Z" + } + ], + "seasonCount": 1 + } + ], + "issues": [ + { + "id": 1, + "issueType": 2, + "status": 1, + "problemSeason": 1, + "problemEpisode": 0, + "createdAt": "2024-12-28T14:22:29.000Z", + "updatedAt": "2024-12-28T14:22:54.000Z", + "media": { + "downloadStatus": [], + "downloadStatus4k": [], + "id": 535, + "mediaType": "tv", + "tmdbId": 249522, + "tvdbId": 447806, + "imdbId": null, + "status": 4, + "status4k": 1, + "createdAt": "2024-12-26T14:37:30.000Z", + "updatedAt": "2024-12-26T14:45:00.000Z", + "lastSeasonChange": "2024-12-26T14:37:30.000Z", + "mediaAddedAt": "2024-12-26T14:39:56.000Z", + "serviceId": 0, + "serviceId4k": null, + "externalServiceId": 144, + "externalServiceId4k": null, + "externalServiceSlug": "beast-games", + "externalServiceSlug4k": null, + "ratingKey": "10189", + "ratingKey4k": null, + "plexUrl": "https://app.plex.tv/desktop#!/server/aaaaaaa/details?key=%2Flibrary%2Fmetadata%2F10189", + "iOSPlexUrl": "plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F10189&server=aaaaaaa", + "serviceUrl": "http://192.168.0.2:8989/series/beast-games" + }, + "createdBy": { + "permissions": 2, + "id": 1, + "email": "me@email.com", + "plexUsername": "somebody", + "username": null, + "recoveryLinkExpirationDate": null, + "userType": 1, + "plexId": 123123, + "avatar": "https://plex.tv/users/123123/avatar?c=123123", + "movieQuotaLimit": null, + "movieQuotaDays": null, + "tvQuotaLimit": null, + "tvQuotaDays": null, + "createdAt": "2024-12-16T21:13:58.000Z", + "updatedAt": "2024-12-16T23:59:03.000Z", + "requestCount": 11, + "displayName": "somebody" + }, + "modifiedBy": null, + "comments": [ + { + "id": 2, + "message": "boop", + "createdAt": "2024-12-28T14:22:54.000Z", + "updatedAt": "2024-12-28T14:22:54.000Z" + }, + { + "id": 1, + "message": "test", + "createdAt": "2024-12-28T14:22:29.000Z", + "updatedAt": "2024-12-28T14:22:29.000Z" + } + ] + } + ], + "seasons": [ + { + "id": 577, + "seasonNumber": 1, + "status": 4, + "status4k": 1, + "createdAt": "2024-12-26T14:45:00.000Z", + "updatedAt": "2024-12-26T14:45:00.000Z" + } + ], + "plexUrl": "https://app.plex.tv/desktop#!/server/aaaaaaa/details?key=%2Flibrary%2Fmetadata%2F10189", + "iOSPlexUrl": "plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F10189&server=aaaaaaa", + "serviceUrl": "http://192.168.0.2:8989/series/beast-games" + }, + "watchProviders": [ + { + "iso_3166_1": "AD", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AD", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AE", + "buy": [], + "flatrate": [ + { + "displayPriority": 9, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AL", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AL", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AR", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "AT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 65, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "AU", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=AU", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BB", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BB", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BE", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BH", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BM", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BO", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BR", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "BS", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=BS", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CA", + "buy": [], + "flatrate": [ + { + "displayPriority": 3, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 152, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "CH", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CI", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CL", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CL", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CO", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "CZ", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=CZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "DE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=DE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 157, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "DK", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=DK", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "DO", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=DO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "DZ", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=DZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "EC", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=EC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "EE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=EE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "EG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=EG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ES", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=ES", + "buy": [], + "flatrate": [ + { + "displayPriority": 3, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "FI", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=FI", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "FR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=FR", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GB", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GB", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 136, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "GF", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GF", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GH", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GI", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "GT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=GT", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HK", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=HK", + "buy": [], + "flatrate": [ + { + "displayPriority": 5, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HN", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=HN", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=HR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "HU", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=HU", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ID", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=ID", + "buy": [], + "flatrate": [ + { + "displayPriority": 7, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=IE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IL", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=IL", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IN", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=IN", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IQ", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=IQ", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IS", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=IS", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "IT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=IT", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 15, + "logoPath": "/6FDKQWcR6JfmRKLqezSsvGgRuUY.jpg", + "id": 109, + "name": "Timvision" + } + ] + }, + { + "iso_3166_1": "JM", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=JM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "JO", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=JO", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "JP", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=JP", + "buy": [], + "flatrate": [ + { + "displayPriority": 4, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "KE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=KE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "KR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=KR", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "KW", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=KW", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LB", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=LB", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LC", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=LC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LI", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=LI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=LT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LV", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=LV", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "LY", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=LY", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MC", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MD", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MD", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MK", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MK", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MX", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MX", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MY", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MY", + "buy": [], + "flatrate": [ + { + "displayPriority": 9, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "MZ", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=MZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=NE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=NG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NL", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=NL", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NO", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=NO", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "NZ", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=NZ", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "OM", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=OM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PF", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PF", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PH", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PH", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PK", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PK", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PL", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PL", + "buy": [], + "flatrate": [ + { + "displayPriority": 5, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PS", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PS", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PT", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "PY", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=PY", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "QA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=QA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "RO", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=RO", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "RS", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=RS", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SA", + "buy": [], + "flatrate": [ + { + "displayPriority": 23, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SE", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SI", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SI", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SK", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SK", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SM", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "SV", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=SV", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TC", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=TC", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TH", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=TH", + "buy": [], + "flatrate": [ + { + "displayPriority": 3, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TN", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=TN", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TR", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=TR", + "buy": [], + "flatrate": [ + { + "displayPriority": 6, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TT", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=TT", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "TW", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=TW", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "UG", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=UG", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "US", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=US", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 9, + "name": "Amazon Prime Video" + }, + { + "displayPriority": 258, + "logoPath": "/8aBqoNeGGr0oSA85iopgNZUOTOc.jpg", + "id": 2100, + "name": "Amazon Prime Video with Ads" + } + ] + }, + { + "iso_3166_1": "UY", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=UY", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "VA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=VA", + "buy": [], + "flatrate": [ + { + "displayPriority": 0, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "VE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=VE", + "buy": [], + "flatrate": [ + { + "displayPriority": 2, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "YE", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=YE", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ZA", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=ZA", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + }, + { + "iso_3166_1": "ZM", + "link": "https://www.themoviedb.org/tv/249522-beast-games/watch?locale=ZM", + "buy": [], + "flatrate": [ + { + "displayPriority": 1, + "logoPath": "/pvske1MyAoymrs5bguRfVqYiM9a.jpg", + "id": 119, + "name": "Amazon Prime Video" + } + ] + } + ] +} diff --git a/tests/components/overseerr/fixtures/webhook_config.json b/tests/components/overseerr/fixtures/webhook_config.json new file mode 100644 index 00000000000..2b3388444d2 --- /dev/null +++ b/tests/components/overseerr/fixtures/webhook_config.json @@ -0,0 +1,8 @@ +{ + "enabled": true, + "types": 222, + "options": { + "jsonPayload": "{\"notification_type\":\"{{notification_type}}\",\"subject\":\"{{subject}}\",\"message\":\"{{message}}\",\"image\":\"{{image}}\",\"{{media}}\":{\"media_type\":\"{{media_type}}\",\"tmdb_id\":\"{{media_tmdbid}}\",\"tvdb_id\":\"{{media_tvdbid}}\",\"status\":\"{{media_status}}\",\"status4k\":\"{{media_status4k}}\"},\"{{request}}\":{\"request_id\":\"{{request_id}}\",\"requested_by_email\":\"{{requestedBy_email}}\",\"requested_by_username\":\"{{requestedBy_username}}\",\"requested_by_avatar\":\"{{requestedBy_avatar}}\",\"requested_by_settings_discord_id\":\"{{requestedBy_settings_discordId}}\",\"requested_by_settings_telegram_chat_id\":\"{{requestedBy_settings_telegramChatId}}\"},\"{{issue}}\":{\"issue_id\":\"{{issue_id}}\",\"issue_type\":\"{{issue_type}}\",\"issue_status\":\"{{issue_status}}\",\"reported_by_email\":\"{{reportedBy_email}}\",\"reported_by_username\":\"{{reportedBy_username}}\",\"reported_by_avatar\":\"{{reportedBy_avatar}}\",\"reported_by_settings_discord_id\":\"{{reportedBy_settings_discordId}}\",\"reported_by_settings_telegram_chat_id\":\"{{reportedBy_settings_telegramChatId}}\"},\"{{comment}}\":{\"comment_message\":\"{{comment_message}}\",\"commented_by_email\":\"{{commentedBy_email}}\",\"commented_by_username\":\"{{commentedBy_username}}\",\"commented_by_avatar\":\"{{commentedBy_avatar}}\",\"commented_by_settings_discord_id\":\"{{commentedBy_settings_discordId}}\",\"commented_by_settings_telegram_chat_id\":\"{{commentedBy_settings_telegramChatId}}\"}}", + "webhookUrl": "http://10.10.10.10:8123/api/webhook/test-webhook-id" + } +} diff --git a/tests/components/overseerr/fixtures/webhook_request_automatically_approved.json b/tests/components/overseerr/fixtures/webhook_request_automatically_approved.json new file mode 100644 index 00000000000..75059bcaf96 --- /dev/null +++ b/tests/components/overseerr/fixtures/webhook_request_automatically_approved.json @@ -0,0 +1,23 @@ +{ + "notification_type": "MEDIA_AUTO_APPROVED", + "subject": "Something (2024)", + "message": "Here is an interesting Linux ISO that was automatically approved.", + "image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/something.jpg", + "media": { + "media_type": "movie", + "tmdb_id": "123", + "tvdb_id": "", + "status": "PENDING", + "status4k": "UNKNOWN" + }, + "request": { + "request_id": "16", + "requested_by_email": "my@email.com", + "requested_by_username": "henk", + "requested_by_avatar": "https://plex.tv/users/abc/avatar?c=123", + "requested_by_settings_discord_id": "123", + "requested_by_settings_telegram_chat_id": "" + }, + "issue": null, + "comment": null +} diff --git a/tests/components/overseerr/fixtures/webhook_test_notification.json b/tests/components/overseerr/fixtures/webhook_test_notification.json new file mode 100644 index 00000000000..4bae2ed742c --- /dev/null +++ b/tests/components/overseerr/fixtures/webhook_test_notification.json @@ -0,0 +1,12 @@ +{ + "notification_type": "TEST_NOTIFICATION", + "event": "", + "subject": "Test Notification", + "message": "Check check, 1, 2, 3. Are we coming in clear?", + "image": "", + "media": null, + "request": null, + "issue": null, + "comment": null, + "extra": [] +} diff --git a/tests/components/overseerr/snapshots/test_diagnostics.ambr b/tests/components/overseerr/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..164257bb9f1 --- /dev/null +++ b/tests/components/overseerr/snapshots/test_diagnostics.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_diagnostics_polling_instance + dict({ + 'coordinator_data': dict({ + 'approved': 11, + 'available': 8, + 'declined': 0, + 'movie': 9, + 'pending': 0, + 'processing': 3, + 'total': 11, + 'tv': 2, + }), + 'has_cloudhooks': False, + }) +# --- +# name: test_diagnostics_webhook_instance + dict({ + 'coordinator_data': dict({ + 'approved': 11, + 'available': 8, + 'declined': 0, + 'movie': 9, + 'pending': 0, + 'processing': 3, + 'total': 11, + 'tv': 2, + }), + 'has_cloudhooks': True, + }) +# --- diff --git a/tests/components/overseerr/snapshots/test_event.ambr b/tests/components/overseerr/snapshots/test_event.ambr new file mode 100644 index 00000000000..8a7be6c463d --- /dev/null +++ b/tests/components/overseerr/snapshots/test_event.ambr @@ -0,0 +1,84 @@ +# serializer version: 1 +# name: test_entities[event.overseerr_last_media_event-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'pending', + 'approved', + 'available', + 'failed', + 'declined', + 'auto_approved', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.overseerr_last_media_event', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last media event', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_media_event', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-media', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[event.overseerr_last_media_event-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/something.jpg', + 'event_type': 'auto_approved', + 'event_types': list([ + 'pending', + 'approved', + 'available', + 'failed', + 'declined', + 'auto_approved', + ]), + 'friendly_name': 'Overseerr Last media event', + 'media': dict({ + 'media_type': 'movie', + 'status': 'pending', + 'status4k': 'unknown', + 'tmdb_id': 123, + 'tvdb_id': None, + }), + 'message': 'Here is an interesting Linux ISO that was automatically approved.', + 'request': dict({ + 'request_id': 16, + 'requested_by_avatar': 'https://plex.tv/users/abc/avatar?c=123', + 'requested_by_email': 'my@email.com', + 'requested_by_settings_discord_id': '123', + 'requested_by_settings_telegram_chat_id': '', + 'requested_by_username': 'henk', + }), + 'subject': 'Something (2024)', + }), + 'context': , + 'entity_id': 'event.overseerr_last_media_event', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- diff --git a/tests/components/overseerr/snapshots/test_init.ambr b/tests/components/overseerr/snapshots/test_init.ambr new file mode 100644 index 00000000000..2709f532ef6 --- /dev/null +++ b/tests/components/overseerr/snapshots/test_init.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'http://overseerr.test', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'overseerr', + '01JG00V55WEVTJ0CJHM0GAD7PC', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'Overseerr', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/overseerr/snapshots/test_sensor.ambr b/tests/components/overseerr/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..bbee260b782 --- /dev/null +++ b/tests/components/overseerr/snapshots/test_sensor.ambr @@ -0,0 +1,358 @@ +# serializer version: 1 +# name: test_all_entities[sensor.overseerr_available_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_available_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Available requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'available_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-available_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_available_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr Available requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_available_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_all_entities[sensor.overseerr_declined_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_declined_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Declined requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'declined_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-declined_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_declined_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr Declined requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_declined_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.overseerr_movie_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_movie_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Movie requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'movie_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-movie_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_movie_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr Movie requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_movie_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9', + }) +# --- +# name: test_all_entities[sensor.overseerr_pending_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_pending_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pending requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pending_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-pending_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_pending_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr Pending requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_pending_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.overseerr_processing_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_processing_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Processing requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'processing_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-processing_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_processing_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr Processing requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_processing_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_all_entities[sensor.overseerr_total_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_total_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-total_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_total_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr Total requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_total_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '11', + }) +# --- +# name: test_all_entities[sensor.overseerr_tv_requests-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.overseerr_tv_requests', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TV requests', + 'platform': 'overseerr', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tv_requests', + 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-tv_requests', + 'unit_of_measurement': 'requests', + }) +# --- +# name: test_all_entities[sensor.overseerr_tv_requests-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Overseerr TV requests', + 'state_class': , + 'unit_of_measurement': 'requests', + }), + 'context': , + 'entity_id': 'sensor.overseerr_tv_requests', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- diff --git a/tests/components/overseerr/snapshots/test_services.ambr b/tests/components/overseerr/snapshots/test_services.ambr new file mode 100644 index 00000000000..5a0b0ce6586 --- /dev/null +++ b/tests/components/overseerr/snapshots/test_services.ambr @@ -0,0 +1,204 @@ +# serializer version: 1 +# name: test_service_get_requests + dict({ + 'requests': list([ + dict({ + 'created_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc), + 'id': 16, + 'is4k': False, + 'media': dict({ + 'adult': False, + 'budget': 0, + 'genres': list([ + dict({ + 'id': 10749, + 'name': 'Romance', + }), + dict({ + 'id': 18, + 'name': 'Drama', + }), + ]), + 'id': 1156593, + 'imdb_id': 'tt28510079', + 'keywords': list([ + dict({ + 'id': 818, + 'name': 'based on novel or book', + }), + dict({ + 'id': 9663, + 'name': 'sequel', + }), + ]), + 'media_info': dict({ + 'created_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc), + 'id': 537, + 'imdb_id': None, + 'media_type': , + 'status': , + 'tmdb_id': 1156593, + 'tvdb_id': None, + 'updated_at': datetime.datetime(2024, 12, 29, 10, 4, 17, tzinfo=datetime.timezone.utc), + }), + 'original_language': 'es', + 'original_title': 'Culpa tuya', + 'overview': "The love between Noah and Nick seems unwavering despite their parents' attempts to separate them. But his job and her entry into college open up their lives to new relationships that will shake the foundations of both their relationship and the Leister family itself.", + 'popularity': 3958.479, + 'release_date': datetime.date(2024, 12, 26), + 'revenue': 0, + 'runtime': 120, + 'tagline': 'Divided by family. Driven by love.', + 'title': 'Your Fault', + 'vote_average': 7.7, + 'vote_count': 190, + }), + 'modified_by': dict({ + 'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa', + 'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc), + 'display_name': 'somebody', + 'email': 'one@email.com', + 'id': 1, + 'movie_quota_days': None, + 'movie_quota_limit': None, + 'plex_id': 321321321, + 'plex_username': 'somebody', + 'request_count': 11, + 'tv_quota_days': None, + 'tv_quota_limit': None, + 'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc), + }), + 'requested_by': dict({ + 'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa', + 'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc), + 'display_name': 'somebody', + 'email': 'one@email.com', + 'id': 1, + 'movie_quota_days': None, + 'movie_quota_limit': None, + 'plex_id': 321321321, + 'plex_username': 'somebody', + 'request_count': 11, + 'tv_quota_days': None, + 'tv_quota_limit': None, + 'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc), + }), + 'season_count': 0, + 'status': , + 'updated_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc), + }), + dict({ + 'created_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc), + 'id': 14, + 'is4k': False, + 'media': dict({ + 'first_air_date': datetime.date(2024, 12, 19), + 'genres': list([ + dict({ + 'id': 10764, + 'name': 'Reality', + }), + ]), + 'id': 249522, + 'keywords': list([ + dict({ + 'id': 271, + 'name': 'competition', + }), + dict({ + 'id': 4325, + 'name': 'game show', + }), + dict({ + 'id': 330122, + 'name': 'mrbeast', + }), + ]), + 'languages': list([ + 'da', + 'en', + ]), + 'last_air_date': datetime.date(2024, 12, 26), + 'last_episode_to_air': dict({ + 'air_date': datetime.date(2024, 12, 26), + 'episode_number': 3, + 'id': 5802152, + 'name': 'The Solitary Experiment', + 'overview': 'What would happen if three best friends were trapped in a room, but only two could escape? Watch and see for yourself right now!', + 'still_path': '/r6LRRaA2l2tMDttWbYl3dXdJUij.jpg', + }), + 'media_info': dict({ + 'created_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc), + 'id': 535, + 'imdb_id': None, + 'media_type': , + 'status': , + 'tmdb_id': 249522, + 'tvdb_id': 447806, + 'updated_at': datetime.datetime(2024, 12, 26, 14, 45, tzinfo=datetime.timezone.utc), + }), + 'name': 'Beast Games', + 'next_episode_to_air': dict({ + 'air_date': datetime.date(2025, 1, 2), + 'episode_number': 4, + 'id': 5802153, + 'name': 'Episode 4', + 'overview': '', + 'still_path': 'None', + }), + 'number_of_episodes': 10, + 'number_of_seasons': 1, + 'original_language': 'en', + 'original_name': 'Beast Games', + 'overview': "I gathered 1,000 people to fight for $5,000,000, the LARGEST cash prize in TV history! We're also giving away a private island, Lamborghinis, and millions more in cash throughout the competition! Go watch to see the greatest show ever made!", + 'popularity': 769.189, + 'seasons': list([ + dict({ + 'air_date': datetime.date(2024, 12, 19), + 'episode_count': 10, + 'id': 384427, + 'name': 'Season 1', + 'overview': '', + 'poster_path': '/3itZlypnOcVcqI5xxyO6nvJ52yM.jpg', + 'season_number': 1, + }), + ]), + 'tagline': '1,000 players. 5 million dollars. 1 winner.', + }), + 'modified_by': dict({ + 'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa', + 'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc), + 'display_name': 'somebody', + 'email': 'one@email.com', + 'id': 1, + 'movie_quota_days': None, + 'movie_quota_limit': None, + 'plex_id': 321321321, + 'plex_username': 'somebody', + 'request_count': 11, + 'tv_quota_days': None, + 'tv_quota_limit': None, + 'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc), + }), + 'requested_by': dict({ + 'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa', + 'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc), + 'display_name': 'somebody', + 'email': 'one@email.com', + 'id': 1, + 'movie_quota_days': None, + 'movie_quota_limit': None, + 'plex_id': 321321321, + 'plex_username': 'somebody', + 'request_count': 11, + 'tv_quota_days': None, + 'tv_quota_limit': None, + 'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc), + }), + 'season_count': 1, + 'status': , + 'updated_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc), + }), + ]), + }) +# --- diff --git a/tests/components/overseerr/test_config_flow.py b/tests/components/overseerr/test_config_flow.py new file mode 100644 index 00000000000..6a3b086a8e2 --- /dev/null +++ b/tests/components/overseerr/test_config_flow.py @@ -0,0 +1,302 @@ +"""Tests for the Overseerr config flow.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from python_overseerr.exceptions import ( + OverseerrAuthenticationError, + OverseerrConnectionError, +) + +from homeassistant.components.overseerr.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_WEBHOOK_ID, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import WEBHOOK_ID + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def patch_webhook_id() -> None: + """Patch webhook ID generation.""" + with patch( + "homeassistant.components.overseerr.config_flow.async_generate_id", + return_value=WEBHOOK_ID, + ): + yield + + +async def test_full_flow( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr.test", CONF_API_KEY: "test-key"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Overseerr" + assert result["data"] == { + CONF_HOST: "overseerr.test", + CONF_PORT: 80, + CONF_SSL: False, + CONF_API_KEY: "test-key", + CONF_WEBHOOK_ID: "test-webhook-id", + } + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (OverseerrAuthenticationError, "invalid_auth"), + (OverseerrConnectionError, "cannot_connect"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_overseerr_client.get_request_count.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr.test", CONF_API_KEY: "test-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_overseerr_client.get_request_count.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr.test", CONF_API_KEY: "test-key"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_flow_invalid_host( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test flow invalid host.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://", CONF_API_KEY: "test-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"url": "invalid_host"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr.test", CONF_API_KEY: "test-key"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_already_configured( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr.test", CONF_API_KEY: "test-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "new-test-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data[CONF_API_KEY] == "new-test-key" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (OverseerrAuthenticationError, "invalid_auth"), + (OverseerrConnectionError, "cannot_connect"), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test reauth flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_overseerr_client.get_request_count.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "new-test-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_overseerr_client.get_request_count.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "new-test-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data[CONF_API_KEY] == "new-test-key" + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr2.test", CONF_API_KEY: "new-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == { + CONF_HOST: "overseerr2.test", + CONF_PORT: 80, + CONF_SSL: False, + CONF_API_KEY: "new-key", + CONF_WEBHOOK_ID: WEBHOOK_ID, + } + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (OverseerrAuthenticationError, "invalid_auth"), + (OverseerrConnectionError, "cannot_connect"), + ], +) +async def test_reconfigure_flow_errors( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test reconfigure flow errors.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + mock_overseerr_client.get_request_count.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr2.test", CONF_API_KEY: "new-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_overseerr_client.get_request_count.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_URL: "http://overseerr2.test", CONF_API_KEY: "new-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" diff --git a/tests/components/overseerr/test_diagnostics.py b/tests/components/overseerr/test_diagnostics.py new file mode 100644 index 00000000000..28b97e9514f --- /dev/null +++ b/tests/components/overseerr/test_diagnostics.py @@ -0,0 +1,47 @@ +"""Tests for the diagnostics data provided by the Overseerr integration.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics_polling_instance( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + await setup_integration(hass, mock_config_entry) + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) + + +async def test_diagnostics_webhook_instance( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_overseerr_client_cloudhook: AsyncMock, + mock_cloudhook_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + await setup_integration(hass, mock_cloudhook_config_entry) + + assert ( + await get_diagnostics_for_config_entry( + hass, hass_client, mock_cloudhook_config_entry + ) + == snapshot + ) diff --git a/tests/components/overseerr/test_event.py b/tests/components/overseerr/test_event.py new file mode 100644 index 00000000000..3866ccc09ca --- /dev/null +++ b/tests/components/overseerr/test_event.py @@ -0,0 +1,170 @@ +"""Tests for the Overseerr event platform.""" + +from datetime import UTC, datetime +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from future.backports.datetime import timedelta +import pytest +from python_overseerr import OverseerrConnectionError +from syrupy import SnapshotAssertion + +from homeassistant.components.overseerr import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import call_webhook, setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, + snapshot_platform, +) +from tests.typing import ClientSessionGenerator + + +@pytest.mark.freeze_time("2023-10-21") +async def test_entities( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.overseerr.PLATFORMS", [Platform.EVENT]): + await setup_integration(hass, mock_config_entry) + + client = await hass_client_no_auth() + + await call_webhook( + hass, + load_json_object_fixture("webhook_request_automatically_approved.json", DOMAIN), + client, + ) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.freeze_time("2023-10-21") +async def test_event_does_not_write_state( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test event entities don't write state on coordinator update.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_client_no_auth() + + await call_webhook( + hass, + load_json_object_fixture("webhook_request_automatically_approved.json", DOMAIN), + client, + ) + await hass.async_block_till_done() + + assert hass.states.get( + "event.overseerr_last_media_event" + ).last_reported == datetime(2023, 10, 21, 0, 0, 0, tzinfo=UTC) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get( + "event.overseerr_last_media_event" + ).last_reported == datetime(2023, 10, 21, 0, 0, 0, tzinfo=UTC) + + +async def test_event_goes_unavailable( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test event entities go unavailable when we can't fetch data.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("event.overseerr_last_media_event").state != STATE_UNAVAILABLE + ) + + mock_overseerr_client.get_request_count.side_effect = OverseerrConnectionError( + "Boom" + ) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("event.overseerr_last_media_event").state == STATE_UNAVAILABLE + ) + + +async def test_not_push_based( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client_needs_change: AsyncMock, +) -> None: + """Test event entities aren't created if not push based.""" + + mock_overseerr_client_needs_change.test_webhook_notification_config.return_value = ( + False + ) + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.overseerr_last_media_event") is None + + +async def test_cant_fetch_webhook_config( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, +) -> None: + """Test event entities aren't created if not push based.""" + + mock_overseerr_client.get_webhook_notification_config.side_effect = ( + OverseerrConnectionError("Boom") + ) + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.overseerr_last_media_event") is None + + +async def test_not_push_based_but_was_before( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client_needs_change: AsyncMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test event entities are created if push based in the past.""" + + entity_registry.async_get_or_create( + Platform.EVENT, + DOMAIN, + f"{mock_config_entry.entry_id}-media", + suggested_object_id="overseerr_last_media_event", + disabled_by=None, + ) + + mock_overseerr_client_needs_change.test_webhook_notification_config.return_value = ( + False + ) + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.overseerr_last_media_event") is not None + + assert ( + hass.states.get("event.overseerr_last_media_event").state == STATE_UNAVAILABLE + ) diff --git a/tests/components/overseerr/test_init.py b/tests/components/overseerr/test_init.py new file mode 100644 index 00000000000..6418e2103db --- /dev/null +++ b/tests/components/overseerr/test_init.py @@ -0,0 +1,412 @@ +"""Tests for the Overseerr integration.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest +from python_overseerr import OverseerrAuthenticationError, OverseerrConnectionError +from python_overseerr.models import WebhookNotificationOptions +from syrupy import SnapshotAssertion + +from homeassistant.components import cloud +from homeassistant.components.cloud import CloudNotAvailable +from homeassistant.components.overseerr import ( + CONF_CLOUDHOOK_URL, + JSON_PAYLOAD, + REGISTERED_NOTIFICATIONS, +) +from homeassistant.components.overseerr.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.cloud import mock_cloud + + +@pytest.mark.parametrize( + ("exception", "config_entry_state"), + [ + (OverseerrAuthenticationError, ConfigEntryState.SETUP_ERROR), + (OverseerrConnectionError, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_initialization_errors( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + config_entry_state: ConfigEntryState, +) -> None: + """Test the Overseerr integration initialization errors.""" + mock_overseerr_client.get_request_count.side_effect = exception + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state == config_entry_state + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + +async def test_proper_webhook_configuration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, +) -> None: + """Test the webhook configuration.""" + await setup_integration(hass, mock_config_entry) + + assert REGISTERED_NOTIFICATIONS == 222 + + mock_overseerr_client.test_webhook_notification_config.assert_not_called() + mock_overseerr_client.set_webhook_notification_config.assert_not_called() + + +@pytest.mark.parametrize( + "update_mock", + [ + {"return_value.enabled": False}, + {"return_value.types": 4}, + {"return_value.types": 4062}, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://example.com", json_payload=JSON_PAYLOAD + ) + }, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://10.10.10.10:8123/api/webhook/test-webhook-id", + json_payload='"{\\"message\\": \\"{{title}}\\"}"', + ) + }, + ], + ids=[ + "Disabled", + "Smaller scope", + "Bigger scope", + "Webhook URL", + "JSON Payload", + ], +) +async def test_webhook_configuration_need_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, + update_mock: dict[str, Any], +) -> None: + """Test the webhook configuration.""" + mock_overseerr_client.get_webhook_notification_config.configure_mock(**update_mock) + + await setup_integration(hass, mock_config_entry) + + mock_overseerr_client.test_webhook_notification_config.assert_called_once() + mock_overseerr_client.set_webhook_notification_config.assert_called_once() + + +@pytest.mark.parametrize( + "update_mock", + [ + {"return_value.enabled": False}, + {"return_value.types": 4}, + {"return_value.types": 4062}, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://example.com", json_payload=JSON_PAYLOAD + ) + }, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://10.10.10.10:8123/api/webhook/test-webhook-id", + json_payload='"{\\"message\\": \\"{{title}}\\"}"', + ) + }, + ], + ids=[ + "Disabled", + "Smaller scope", + "Bigger scope", + "Webhook URL", + "JSON Payload", + ], +) +async def test_webhook_failing_test( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, + update_mock: dict[str, Any], +) -> None: + """Test the webhook configuration.""" + mock_overseerr_client.test_webhook_notification_config.return_value = False + mock_overseerr_client.get_webhook_notification_config.configure_mock(**update_mock) + + await setup_integration(hass, mock_config_entry) + + mock_overseerr_client.test_webhook_notification_config.assert_called_once() + mock_overseerr_client.set_webhook_notification_config.assert_not_called() + + +async def test_prefer_internal_ip( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, +) -> None: + """Test the integration prefers internal IP.""" + mock_overseerr_client.test_webhook_notification_config.return_value = False + hass.config.internal_url = "http://192.168.0.123:8123" + hass.config.external_url = "https://www.example.com" + await hass.async_block_till_done(wait_background_tasks=True) + await setup_integration(hass, mock_config_entry) + + assert ( + mock_overseerr_client.test_webhook_notification_config.call_args_list[0][0][0] + == "http://192.168.0.123:8123/api/webhook/test-webhook-id" + ) + assert ( + mock_overseerr_client.test_webhook_notification_config.call_args_list[1][0][0] + == "https://www.example.com/api/webhook/test-webhook-id" + ) + + +async def test_cloudhook_setup( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client_needs_change: AsyncMock, +) -> None: + """Test if set up with active cloud subscription and cloud hook.""" + + await mock_cloud(hass) + await hass.async_block_till_done() + + mock_overseerr_client_needs_change.test_webhook_notification_config.side_effect = [ + False, + True, + ] + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, + ): + await setup_integration(hass, mock_config_entry) + + assert cloud.async_active_subscription(hass) is True + + assert ( + mock_config_entry.data[CONF_CLOUDHOOK_URL] == "https://hooks.nabu.casa/ABCD" + ) + + assert ( + len( + mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls + ) + == 2 + ) + + assert hass.config_entries.async_entries(DOMAIN) + fake_create_cloudhook.assert_called() + + for config_entry in hass.config_entries.async_entries(DOMAIN): + await hass.config_entries.async_remove(config_entry.entry_id) + fake_delete_cloudhook.assert_called_once() + + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + + +async def test_cloudhook_consistent( + hass: HomeAssistant, + mock_cloudhook_config_entry: MockConfigEntry, + mock_overseerr_client_needs_change: AsyncMock, +) -> None: + """Test if we keep the cloudhook if it is already set up.""" + + await mock_cloud(hass) + await hass.async_block_till_done() + + mock_overseerr_client_needs_change.test_webhook_notification_config.side_effect = [ + False, + True, + ] + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + ): + await setup_integration(hass, mock_cloudhook_config_entry) + + assert cloud.async_active_subscription(hass) is True + + assert ( + mock_cloudhook_config_entry.data[CONF_CLOUDHOOK_URL] + == "https://hooks.nabu.casa/ABCD" + ) + + assert ( + len( + mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls + ) + == 2 + ) + + assert hass.config_entries.async_entries(DOMAIN) + fake_create_cloudhook.assert_not_called() + + +async def test_cloudhook_needs_no_change( + hass: HomeAssistant, + mock_cloudhook_config_entry: MockConfigEntry, + mock_overseerr_client_cloudhook: AsyncMock, +) -> None: + """Test if we keep the cloudhook if it is already set up.""" + + await setup_integration(hass, mock_cloudhook_config_entry) + + assert ( + len(mock_overseerr_client_cloudhook.test_webhook_notification_config.mock_calls) + == 0 + ) + + +async def test_cloudhook_not_needed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client_needs_change: AsyncMock, +) -> None: + """Test if we prefer local webhook over cloudhook.""" + + await hass.async_block_till_done() + + with ( + patch.object(cloud, "async_active_subscription", return_value=True), + ): + await setup_integration(hass, mock_config_entry) + + assert cloud.async_active_subscription(hass) is True + + assert CONF_CLOUDHOOK_URL not in mock_config_entry.data + + assert ( + len( + mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls + ) + == 1 + ) + assert ( + mock_overseerr_client_needs_change.test_webhook_notification_config.call_args_list[ + 0 + ][0][0] + == "http://10.10.10.10:8123/api/webhook/test-webhook-id" + ) + + +async def test_cloudhook_not_connecting( + hass: HomeAssistant, + mock_cloudhook_config_entry: MockConfigEntry, + mock_overseerr_client_needs_change: AsyncMock, +) -> None: + """Test the cloudhook is not registered if Overseerr cannot connect to it.""" + + await mock_cloud(hass) + await hass.async_block_till_done() + + mock_overseerr_client_needs_change.test_webhook_notification_config.return_value = ( + False + ) + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + ): + await setup_integration(hass, mock_cloudhook_config_entry) + + assert cloud.async_active_subscription(hass) is True + + assert ( + mock_cloudhook_config_entry.data[CONF_CLOUDHOOK_URL] + == "https://hooks.nabu.casa/ABCD" + ) + + assert ( + len( + mock_overseerr_client_needs_change.test_webhook_notification_config.mock_calls + ) + == 3 + ) + + mock_overseerr_client_needs_change.set_webhook_notification_config.assert_not_called() + + assert hass.config_entries.async_entries(DOMAIN) + fake_create_cloudhook.assert_not_called() + + +async def test_removing_entry_with_cloud_unavailable( + hass: HomeAssistant, + mock_cloudhook_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, +) -> None: + """Test handling cloud unavailable when deleting entry.""" + + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + side_effect=CloudNotAvailable(), + ), + ): + await setup_integration(hass, mock_cloudhook_config_entry) + + assert cloud.async_active_subscription(hass) is True + + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN) + + for config_entry in hass.config_entries.async_entries(DOMAIN): + await hass.config_entries.async_remove(config_entry.entry_id) + + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/overseerr/test_sensor.py b/tests/components/overseerr/test_sensor.py new file mode 100644 index 00000000000..6689b1ebcc3 --- /dev/null +++ b/tests/components/overseerr/test_sensor.py @@ -0,0 +1,53 @@ +"""Tests for the Overseerr sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.overseerr import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import call_webhook, setup_integration + +from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform +from tests.typing import ClientSessionGenerator + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.overseerr.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_webhook_trigger_update( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.overseerr_available_requests").state == "8" + + mock_overseerr_client.get_request_count.return_value.available = 7 + client = await hass_client_no_auth() + + await call_webhook( + hass, + load_json_object_fixture("webhook_request_automatically_approved.json", DOMAIN), + client, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.overseerr_available_requests").state == "7" diff --git a/tests/components/overseerr/test_services.py b/tests/components/overseerr/test_services.py new file mode 100644 index 00000000000..a0b87b5deef --- /dev/null +++ b/tests/components/overseerr/test_services.py @@ -0,0 +1,156 @@ +"""Tests for the Overseerr services.""" + +from unittest.mock import AsyncMock + +import pytest +from python_overseerr import OverseerrConnectionError +from syrupy import SnapshotAssertion + +from homeassistant.components.overseerr.const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_REQUESTED_BY, + ATTR_SORT_ORDER, + ATTR_STATUS, + DOMAIN, +) +from homeassistant.components.overseerr.services import SERVICE_GET_REQUESTS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_service_get_requests( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the get_requests service.""" + + await setup_integration(hass, mock_config_entry) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_REQUESTS, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_STATUS: "approved", + ATTR_SORT_ORDER: "added", + ATTR_REQUESTED_BY: 1, + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + for request in response["requests"]: + assert "requests" not in request["media"]["media_info"] + mock_overseerr_client.get_requests.assert_called_once_with( + status="approved", sort="added", requested_by=1 + ) + + +async def test_service_get_requests_no_meta( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the get_requests service.""" + mock_overseerr_client.get_movie_details.side_effect = OverseerrConnectionError + mock_overseerr_client.get_tv_details.side_effect = OverseerrConnectionError + + await setup_integration(hass, mock_config_entry) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_REQUESTS, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id}, + blocking=True, + return_response=True, + ) + for request in response["requests"]: + assert request["media"] == {} + + +@pytest.mark.parametrize( + ("service", "payload", "function", "exception", "raised_exception", "message"), + [ + ( + SERVICE_GET_REQUESTS, + {}, + "get_requests", + OverseerrConnectionError("Timeout"), + HomeAssistantError, + "Error connecting to the Overseerr instance: Timeout", + ) + ], +) +async def test_services_connection_error( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + payload: dict[str, str], + function: str, + exception: Exception, + raised_exception: type[Exception], + message: str, +) -> None: + """Test a connection error in the services.""" + + await setup_integration(hass, mock_config_entry) + + getattr(mock_overseerr_client, function).side_effect = exception + + with pytest.raises(raised_exception, match=message): + await hass.services.async_call( + DOMAIN, + service, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload, + blocking=True, + return_response=True, + ) + + +@pytest.mark.parametrize( + ("service", "payload"), + [ + (SERVICE_GET_REQUESTS, {}), + ], +) +async def test_service_entry_availability( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + payload: dict[str, str], +) -> None: + """Test the services without valid entry.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry2 = MockConfigEntry(domain=DOMAIN) + mock_config_entry2.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(ServiceValidationError, match="Mock Title is not loaded"): + await hass.services.async_call( + DOMAIN, + service, + {ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload, + blocking=True, + return_response=True, + ) + + with pytest.raises( + ServiceValidationError, match='Integration "overseerr" not found in registry' + ): + await hass.services.async_call( + DOMAIN, + service, + {ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload, + blocking=True, + return_response=True, + ) diff --git a/tests/components/palazzetti/conftest.py b/tests/components/palazzetti/conftest.py index fad535df914..d3694653cd4 100644 --- a/tests/components/palazzetti/conftest.py +++ b/tests/components/palazzetti/conftest.py @@ -79,7 +79,12 @@ def mock_palazzetti_client() -> Generator[AsyncMock]: mock_client.target_temperature_max = 50 mock_client.pellet_quantity = 1248 mock_client.pellet_level = 0 + mock_client.has_second_fan = True + mock_client.has_second_fan = False mock_client.fan_speed = 3 + mock_client.current_fan_speed.return_value = 3 + mock_client.min_fan_speed.return_value = 0 + mock_client.max_fan_speed.return_value = 5 mock_client.connect.return_value = True mock_client.update_state.return_value = True mock_client.set_on.return_value = True diff --git a/tests/components/palazzetti/snapshots/test_button.ambr b/tests/components/palazzetti/snapshots/test_button.ambr new file mode 100644 index 00000000000..8130f0a0ec7 --- /dev/null +++ b/tests/components/palazzetti/snapshots/test_button.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_all_entities[button.stove_silent-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.stove_silent', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Silent', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'silent', + 'unique_id': '11:22:33:44:55:66-silent', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.stove_silent-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Stove Silent', + }), + 'context': , + 'entity_id': 'button.stove_silent', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/palazzetti/snapshots/test_climate.ambr b/tests/components/palazzetti/snapshots/test_climate.ambr index e7cea3749a1..cf23cb87ccb 100644 --- a/tests/components/palazzetti/snapshots/test_climate.ambr +++ b/tests/components/palazzetti/snapshots/test_climate.ambr @@ -6,7 +6,6 @@ 'area_id': None, 'capabilities': dict({ 'fan_modes': list([ - 'silent', '1', '2', '3', @@ -24,6 +23,7 @@ 'target_temp_step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,7 +56,6 @@ 'current_temperature': 18, 'fan_mode': '3', 'fan_modes': list([ - 'silent', '1', '2', '3', diff --git a/tests/components/palazzetti/snapshots/test_init.ambr b/tests/components/palazzetti/snapshots/test_init.ambr index abdee6b7f6f..fc96cab4fad 100644 --- a/tests/components/palazzetti/snapshots/test_init.ambr +++ b/tests/components/palazzetti/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( diff --git a/tests/components/palazzetti/snapshots/test_number.ambr b/tests/components/palazzetti/snapshots/test_number.ambr index 0a25a1cfa8b..1d40e9e4b6b 100644 --- a/tests/components/palazzetti/snapshots/test_number.ambr +++ b/tests/components/palazzetti/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,3 +56,117 @@ 'state': '3', }) # --- +# name: test_all_entities[number.stove_left_fan_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 5, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.stove_left_fan_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Left fan speed', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_left_speed', + 'unique_id': '11:22:33:44:55:66-fan_left_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.stove_left_fan_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'wind_speed', + 'friendly_name': 'Stove Left fan speed', + 'max': 5, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.stove_left_fan_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_all_entities[number.stove_right_fan_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 5, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.stove_right_fan_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Right fan speed', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan_right_speed', + 'unique_id': '11:22:33:44:55:66-fan_right_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.stove_right_fan_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'wind_speed', + 'friendly_name': 'Stove Right fan speed', + 'max': 5, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.stove_right_fan_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- diff --git a/tests/components/palazzetti/snapshots/test_sensor.ambr b/tests/components/palazzetti/snapshots/test_sensor.ambr index aa98f3a4f59..6bf4f68c1fa 100644 --- a/tests/components/palazzetti/snapshots/test_sensor.ambr +++ b/tests/components/palazzetti/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +164,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -212,6 +216,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -263,6 +268,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -362,6 +368,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -460,6 +467,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -511,6 +519,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/palazzetti/test_button.py b/tests/components/palazzetti/test_button.py new file mode 100644 index 00000000000..de0f26fe8aa --- /dev/null +++ b/tests/components/palazzetti/test_button.py @@ -0,0 +1,69 @@ +"""Tests for the Palazzetti button platform.""" + +from unittest.mock import AsyncMock, patch + +from pypalazzetti.exceptions import CommunicationError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +ENTITY_ID = "button.stove_silent" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.palazzetti.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_async_press( + hass: HomeAssistant, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing via service call.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + mock_palazzetti_client.set_fan_silent.assert_called_once() + + +async def test_async_press_error( + hass: HomeAssistant, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing with error via service call.""" + await setup_integration(hass, mock_config_entry) + + mock_palazzetti_client.set_fan_silent.side_effect = CommunicationError() + error_message = "Could not connect to the device" + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) diff --git a/tests/components/palazzetti/test_climate.py b/tests/components/palazzetti/test_climate.py index 78af8f00bdb..22bd04f234e 100644 --- a/tests/components/palazzetti/test_climate.py +++ b/tests/components/palazzetti/test_climate.py @@ -15,7 +15,7 @@ from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, HVACMode, ) -from homeassistant.components.palazzetti.const import FAN_AUTO, FAN_HIGH, FAN_SILENT +from homeassistant.components.palazzetti.const import FAN_AUTO, FAN_HIGH from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -118,15 +118,6 @@ async def test_async_set_data( ) # Set Fan Mode: Success - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: FAN_SILENT}, - blocking=True, - ) - mock_palazzetti_client.set_fan_silent.assert_called_once() - mock_palazzetti_client.set_fan_silent.reset_mock() - await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/palazzetti/test_config_flow.py b/tests/components/palazzetti/test_config_flow.py index 03c56c33d0c..8550f1a3de0 100644 --- a/tests/components/palazzetti/test_config_flow.py +++ b/tests/components/palazzetti/test_config_flow.py @@ -4,12 +4,12 @@ from unittest.mock import AsyncMock from pypalazzetti.exceptions import CommunicationError -from homeassistant.components import dhcp from homeassistant.components.palazzetti.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -101,7 +101,7 @@ async def test_dhcp_flow( """Test the DHCP flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="connbox1234", ip="192.168.1.1", macaddress="11:22:33:44:55:66" ), context={"source": SOURCE_DHCP}, @@ -130,7 +130,7 @@ async def test_dhcp_flow_error( result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="connbox1234", ip="192.168.1.1", macaddress="11:22:33:44:55:66" ), context={"source": SOURCE_DHCP}, diff --git a/tests/components/palazzetti/test_number.py b/tests/components/palazzetti/test_number.py index 939c7c72c19..8f09384c1b7 100644 --- a/tests/components/palazzetti/test_number.py +++ b/tests/components/palazzetti/test_number.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch from pypalazzetti.exceptions import CommunicationError, ValidationError +from pypalazzetti.fan import FanType import pytest from syrupy import SnapshotAssertion @@ -16,7 +17,8 @@ from . import setup_integration from tests.common import MockConfigEntry, snapshot_platform -ENTITY_ID = "number.stove_combustion_power" +POWER_ENTITY_ID = "number.stove_combustion_power" +FAN_ENTITY_ID = "number.stove_left_fan_speed" async def test_all_entities( @@ -33,7 +35,7 @@ async def test_all_entities( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_async_set_data( +async def test_async_set_data_power( hass: HomeAssistant, mock_palazzetti_client: AsyncMock, mock_config_entry: MockConfigEntry, @@ -45,28 +47,71 @@ async def test_async_set_data( await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: ENTITY_ID, "value": 1}, + {ATTR_ENTITY_ID: POWER_ENTITY_ID, "value": 1}, blocking=True, ) mock_palazzetti_client.set_power_mode.assert_called_once_with(1) - mock_palazzetti_client.set_on.reset_mock() + mock_palazzetti_client.set_power_mode.reset_mock() # Set value: Error mock_palazzetti_client.set_power_mode.side_effect = CommunicationError() - with pytest.raises(HomeAssistantError): + message = "Could not connect to the device" + with pytest.raises(HomeAssistantError, match=message): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: ENTITY_ID, "value": 1}, + {ATTR_ENTITY_ID: POWER_ENTITY_ID, "value": 1}, + blocking=True, + ) + mock_palazzetti_client.set_power_mode.reset_mock() + + mock_palazzetti_client.set_power_mode.side_effect = ValidationError() + message = "Combustion power 1.0 is invalid" + with pytest.raises(ServiceValidationError, match=message): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: POWER_ENTITY_ID, "value": 1}, + blocking=True, + ) + + +async def test_async_set_data_fan( + hass: HomeAssistant, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting number data via service call.""" + await setup_integration(hass, mock_config_entry) + + # Set value: Success + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: FAN_ENTITY_ID, "value": 1}, + blocking=True, + ) + mock_palazzetti_client.set_fan_speed.assert_called_once_with(1, FanType.LEFT) + mock_palazzetti_client.set_on.reset_mock() + + # Set value: Error + mock_palazzetti_client.set_fan_speed.side_effect = CommunicationError() + message = "Could not connect to the device" + with pytest.raises(HomeAssistantError, match=message): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: FAN_ENTITY_ID, "value": 1}, blocking=True, ) mock_palazzetti_client.set_on.reset_mock() - mock_palazzetti_client.set_power_mode.side_effect = ValidationError() - with pytest.raises(ServiceValidationError): + mock_palazzetti_client.set_fan_speed.side_effect = ValidationError() + message = "Fan left speed 1.0 is invalid" + with pytest.raises(ServiceValidationError, match=message): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: ENTITY_ID, "value": 1}, + {ATTR_ENTITY_ID: FAN_ENTITY_ID, "value": 1}, blocking=True, ) diff --git a/tests/components/peblar/__init__.py b/tests/components/peblar/__init__.py new file mode 100644 index 00000000000..9180d51e98b --- /dev/null +++ b/tests/components/peblar/__init__.py @@ -0,0 +1 @@ +"""Integration tests for the Peblar integration.""" diff --git a/tests/components/peblar/conftest.py b/tests/components/peblar/conftest.py new file mode 100644 index 00000000000..95daad545b5 --- /dev/null +++ b/tests/components/peblar/conftest.py @@ -0,0 +1,100 @@ +"""Fixtures for the Peblar integration tests.""" + +from __future__ import annotations + +from collections.abc import Generator +from contextlib import nullcontext +from unittest.mock import MagicMock, patch + +from peblar import ( + PeblarEVInterface, + PeblarMeter, + PeblarSystem, + PeblarSystemInformation, + PeblarUserConfiguration, + PeblarVersions, +) +import pytest + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Peblar", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.127", + CONF_PASSWORD: "OMGSPIDERS", + }, + unique_id="23-45-A4O-MOF", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.peblar.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_peblar() -> Generator[MagicMock]: + """Return a mocked Peblar client.""" + with ( + patch("homeassistant.components.peblar.Peblar", autospec=True) as peblar_mock, + patch("homeassistant.components.peblar.config_flow.Peblar", new=peblar_mock), + ): + peblar = peblar_mock.return_value + peblar.available_versions.return_value = PeblarVersions.from_json( + load_fixture("available_versions.json", DOMAIN) + ) + peblar.current_versions.return_value = PeblarVersions.from_json( + load_fixture("current_versions.json", DOMAIN) + ) + peblar.user_configuration.return_value = PeblarUserConfiguration.from_json( + load_fixture("user_configuration.json", DOMAIN) + ) + peblar.system_information.return_value = PeblarSystemInformation.from_json( + load_fixture("system_information.json", DOMAIN) + ) + + api = peblar.rest_api.return_value + api.ev_interface.return_value = PeblarEVInterface.from_json( + load_fixture("ev_interface.json", DOMAIN) + ) + api.meter.return_value = PeblarMeter.from_json( + load_fixture("meter.json", DOMAIN) + ) + api.system.return_value = PeblarSystem.from_json( + load_fixture("system.json", DOMAIN) + ) + + yield peblar + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, + request: pytest.FixtureRequest, +) -> MockConfigEntry: + """Set up the Peblar integration for testing.""" + mock_config_entry.add_to_hass(hass) + + context = nullcontext() + if platform := getattr(request, "param", None): + context = patch("homeassistant.components.peblar.PLATFORMS", [platform]) + + with context: + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/peblar/fixtures/available_versions.json b/tests/components/peblar/fixtures/available_versions.json new file mode 100644 index 00000000000..45b3255167c --- /dev/null +++ b/tests/components/peblar/fixtures/available_versions.json @@ -0,0 +1,4 @@ +{ + "Customization": "Peblar-1.9", + "Firmware": "1.6.2+1+WL-1" +} diff --git a/tests/components/peblar/fixtures/current_versions.json b/tests/components/peblar/fixtures/current_versions.json new file mode 100644 index 00000000000..c54fb71c457 --- /dev/null +++ b/tests/components/peblar/fixtures/current_versions.json @@ -0,0 +1,4 @@ +{ + "Customization": "Peblar-1.9", + "Firmware": "1.6.1+1+WL-1" +} diff --git a/tests/components/peblar/fixtures/ev_interface.json b/tests/components/peblar/fixtures/ev_interface.json new file mode 100644 index 00000000000..901807a7068 --- /dev/null +++ b/tests/components/peblar/fixtures/ev_interface.json @@ -0,0 +1,7 @@ +{ + "ChargeCurrentLimit": 16000, + "ChargeCurrentLimitActual": 6000, + "ChargeCurrentLimitSource": "Current limiter", + "CpState": "State C", + "Force1Phase": false +} diff --git a/tests/components/peblar/fixtures/meter.json b/tests/components/peblar/fixtures/meter.json new file mode 100644 index 00000000000..f426adf9b8a --- /dev/null +++ b/tests/components/peblar/fixtures/meter.json @@ -0,0 +1,14 @@ +{ + "CurrentPhase1": 14242, + "CurrentPhase2": 0, + "CurrentPhase3": 0, + "EnergySession": 381, + "EnergyTotal": 880703, + "PowerPhase1": 3185, + "PowerPhase2": 0, + "PowerPhase3": 0, + "PowerTotal": 3185, + "VoltagePhase1": 223, + "VoltagePhase2": null, + "VoltagePhase3": null +} diff --git a/tests/components/peblar/fixtures/system.json b/tests/components/peblar/fixtures/system.json new file mode 100644 index 00000000000..87bb60575da --- /dev/null +++ b/tests/components/peblar/fixtures/system.json @@ -0,0 +1,12 @@ +{ + "ActiveErrorCodes": [], + "ActiveWarningCodes": [], + "CellularSignalStrength": null, + "FirmwareVersion": "1.6.1+1+WL-1", + "Force1PhaseAllowed": true, + "PhaseCount": 3, + "ProductPn": "6004-2300-8002", + "ProductSn": "23-45-A4O-MOF", + "Uptime": 322094, + "WlanSignalStrength": null +} diff --git a/tests/components/peblar/fixtures/system_information.json b/tests/components/peblar/fixtures/system_information.json new file mode 100644 index 00000000000..dcec52a37fe --- /dev/null +++ b/tests/components/peblar/fixtures/system_information.json @@ -0,0 +1,57 @@ +{ + "BopCalIGainA": 264625, + "BopCalIGainB": 267139, + "BopCalIGainC": 239155, + "CanChangeChargingPhases": false, + "CanChargeSinglePhase": true, + "CanChargeThreePhases": false, + "CustomerId": "PBLR-0000645", + "CustomerUpdatePackagePubKey": "-----BEGIN PUBLIC KEY-----\nlorem ipsum\n-----END PUBLIC KEY-----\n", + "EthMacAddr": "00:0F:11:58:86:97", + "FwIdent": "1.6.1+1+WL-1", + "Hostname": "PBLR-0000645", + "HwFixedCableRating": 20, + "HwFwCompat": "wlac-2", + "HwHas4pRelay": false, + "HwHasBop": true, + "HwHasBuzzer": true, + "HwHasDualSocket": false, + "HwHasEichrechtLaserMarking": false, + "HwHasEthernet": true, + "HwHasLed": true, + "HwHasLte": false, + "HwHasMeter": true, + "HwHasMeterDisplay": true, + "HwHasPlc": false, + "HwHasRfid": true, + "HwHasRs485": true, + "HwHasShutter": false, + "HwHasSocket": false, + "HwHasTpm": false, + "HwHasWlan": true, + "HwMaxCurrent": 16, + "HwOneOrThreePhase": 3, + "HwUKCompliant": false, + "MainboardPn": "6004-2300-7600", + "MainboardSn": "23-38-A4E-2MC", + "MeterCalIGainA": 267369, + "MeterCalIGainB": 228286, + "MeterCalIGainC": 246455, + "MeterCalIRmsOffsetA": 15573, + "MeterCalIRmsOffsetB": 268422963, + "MeterCalIRmsOffsetC": 9082, + "MeterCalPhaseA": 250, + "MeterCalPhaseB": 271, + "MeterCalPhaseC": 271, + "MeterCalVGainA": 250551, + "MeterCalVGainB": 246074, + "MeterCalVGainC": 230191, + "MeterFwIdent": "b9cbcd", + "NorFlash": true, + "ProductModelName": "WLAC1-H11R0WE0ICR00", + "ProductPn": "6004-2300-8002", + "ProductSn": "23-45-A4O-MOF", + "ProductVendorName": "Peblar", + "WlanApMacAddr": "00:0F:11:58:86:98", + "WlanStaMacAddr": "00:0F:11:58:86:99" +} diff --git a/tests/components/peblar/fixtures/user_configuration.json b/tests/components/peblar/fixtures/user_configuration.json new file mode 100644 index 00000000000..b41aecd00ef --- /dev/null +++ b/tests/components/peblar/fixtures/user_configuration.json @@ -0,0 +1,59 @@ +{ + "BopFallbackCurrent": 6000, + "BopHomeWizardAddress": "p1meter-093586", + "BopSource": "homewizard", + "BopSourceParameters": "{}", + "ConnectedPhases": 3, + "CurrentCtrlBopCtType": "CTK05-14", + "CurrentCtrlBopEnable": true, + "CurrentCtrlBopFuseRating": 35, + "CurrentCtrlFixedChargeCurrentLimit": 16, + "GroundMonitoring": true, + "GroupLoadBalancingEnable": false, + "GroupLoadBalancingFallbackCurrent": 6, + "GroupLoadBalancingGroupId": 1, + "GroupLoadBalancingInterface": "RS485", + "GroupLoadBalancingMaxCurrent": 0, + "GroupLoadBalancingRole": "", + "HmiBuzzerVolume": 1, + "HmiLedIntensityManual": 0, + "HmiLedIntensityMax": 100, + "HmiLedIntensityMin": 1, + "HmiLedIntensityMode": "Fixed", + "LocalRestApiAccessMode": "ReadWrite", + "LocalRestApiAllowed": true, + "LocalRestApiEnable": true, + "LocalSmartChargingAllowed": true, + "ModbusServerAccessMode": "ReadOnly", + "ModbusServerAllowed": true, + "ModbusServerEnable": true, + "PhaseRotation": "RST", + "PowerLimitInputDi1Inverse": false, + "PowerLimitInputDi1Limit": 6, + "PowerLimitInputDi2Inverse": false, + "PowerLimitInputDi2Limit": 0, + "PowerLimitInputEnable": false, + "PredefinedCpoName": "", + "ScheduledChargingAllowed": true, + "ScheduledChargingEnable": false, + "SeccOcppActive": false, + "SeccOcppUri": "", + "SessionManagerChargeWithoutAuth": false, + "SolarChargingAllowed": true, + "SolarChargingEnable": true, + "SolarChargingMode": "PureSolar", + "SolarChargingSource": "homewizard", + "SolarChargingSourceParameters": "{\"address\":\"p1meter-093586\"}", + "TimeZone": "Europe/Amsterdam", + "UserDefinedChargeLimitCurrent": 16, + "UserDefinedChargeLimitCurrentAllowed": true, + "UserDefinedHouseholdPowerLimit": 20000, + "UserDefinedHouseholdPowerLimitAllowed": true, + "UserDefinedHouseholdPowerLimitEnable": false, + "UserDefinedHouseholdPowerLimitSource": "homewizard", + "UserDefinedHouseholdPowerLimitSourceParameters": "{\"address\":\"p1meter-093586\"}", + "UserKeepSocketLocked": false, + "VDEPhaseImbalanceEnable": false, + "VDEPhaseImbalanceLimit": 20, + "WebIfUpdateHelper": true +} diff --git a/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr b/tests/components/peblar/snapshots/test_binary_sensor.ambr similarity index 50% rename from tests/components/imgw_pib/snapshots/test_binary_sensor.ambr rename to tests/components/peblar/snapshots/test_binary_sensor.ambr index c5ae6880022..9ad9c877ed2 100644 --- a/tests/components/imgw_pib/snapshots/test_binary_sensor.ambr +++ b/tests/components/peblar/snapshots/test_binary_sensor.ambr @@ -1,17 +1,18 @@ # serializer version: 1 -# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_alarm-entry] +# name: test_entities[binary_sensor][binary_sensor.peblar_ev_charger_active_errors-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.river_name_station_name_flood_alarm', + 'entity_category': , + 'entity_id': 'binary_sensor.peblar_ev_charger_active_errors', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -21,45 +22,45 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Flood alarm', - 'platform': 'imgw_pib', + 'original_name': 'Active errors', + 'platform': 'peblar', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'flood_alarm', - 'unique_id': '123_flood_alarm', + 'translation_key': 'active_error_codes', + 'unique_id': '23-45-A4O-MOF_active_error_codes', 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_alarm-state] +# name: test_entities[binary_sensor][binary_sensor.peblar_ev_charger_active_errors-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'safety', - 'friendly_name': 'River Name (Station Name) Flood alarm', + 'device_class': 'problem', + 'friendly_name': 'Peblar EV Charger Active errors', }), 'context': , - 'entity_id': 'binary_sensor.river_name_station_name_flood_alarm', + 'entity_id': 'binary_sensor.peblar_ev_charger_active_errors', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_warning-entry] +# name: test_entities[binary_sensor][binary_sensor.peblar_ev_charger_active_warnings-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.river_name_station_name_flood_warning', + 'entity_category': , + 'entity_id': 'binary_sensor.peblar_ev_charger_active_warnings', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -69,26 +70,25 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Flood warning', - 'platform': 'imgw_pib', + 'original_name': 'Active warnings', + 'platform': 'peblar', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'flood_warning', - 'unique_id': '123_flood_warning', + 'translation_key': 'active_warning_codes', + 'unique_id': '23-45-A4O-MOF_active_warning_codes', 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[binary_sensor.river_name_station_name_flood_warning-state] +# name: test_entities[binary_sensor][binary_sensor.peblar_ev_charger_active_warnings-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by IMGW-PIB', - 'device_class': 'safety', - 'friendly_name': 'River Name (Station Name) Flood warning', + 'device_class': 'problem', + 'friendly_name': 'Peblar EV Charger Active warnings', }), 'context': , - 'entity_id': 'binary_sensor.river_name_station_name_flood_warning', + 'entity_id': 'binary_sensor.peblar_ev_charger_active_warnings', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/peblar/snapshots/test_button.ambr b/tests/components/peblar/snapshots/test_button.ambr new file mode 100644 index 00000000000..6d31da0ae52 --- /dev/null +++ b/tests/components/peblar/snapshots/test_button.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_entities[button][button.peblar_ev_charger_identify-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.peblar_ev_charger_identify', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Identify', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '23-45-A4O-MOF_identify', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button][button.peblar_ev_charger_identify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'identify', + 'friendly_name': 'Peblar EV Charger Identify', + }), + 'context': , + 'entity_id': 'button.peblar_ev_charger_identify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[button][button.peblar_ev_charger_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.peblar_ev_charger_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '23-45-A4O-MOF_reboot', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button][button.peblar_ev_charger_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Peblar EV Charger Restart', + }), + 'context': , + 'entity_id': 'button.peblar_ev_charger_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/peblar/snapshots/test_diagnostics.ambr b/tests/components/peblar/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..fbcdcfbaff5 --- /dev/null +++ b/tests/components/peblar/snapshots/test_diagnostics.ambr @@ -0,0 +1,168 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'ev': dict({ + 'ChargeCurrentLimit': 16000, + 'ChargeCurrentLimitActual': 6000, + 'ChargeCurrentLimitSource': 'Current limiter', + 'CpState': 'State C', + 'Force1Phase': False, + }), + 'meter': dict({ + 'CurrentPhase1': 14242, + 'CurrentPhase2': 0, + 'CurrentPhase3': 0, + 'EnergySession': 381, + 'EnergyTotal': 880703, + 'PowerPhase1': 3185, + 'PowerPhase2': 0, + 'PowerPhase3': 0, + 'PowerTotal': 3185, + 'VoltagePhase1': 223, + }), + 'system': dict({ + 'ActiveErrorCodes': list([ + ]), + 'ActiveWarningCodes': list([ + ]), + 'FirmwareVersion': '1.6.1+1+WL-1', + 'Force1PhaseAllowed': True, + 'PhaseCount': 3, + 'ProductPn': '6004-2300-8002', + 'ProductSn': '23-45-A4O-MOF', + 'Uptime': 322094, + }), + 'system_information': dict({ + 'BopCalIGainA': 264625, + 'BopCalIGainB': 267139, + 'BopCalIGainC': 239155, + 'CanChangeChargingPhases': False, + 'CanChargeSinglePhase': True, + 'CanChargeThreePhases': False, + 'CustomerId': 'PBLR-0000645', + 'CustomerUpdatePackagePubKey': ''' + -----BEGIN PUBLIC KEY----- + lorem ipsum + -----END PUBLIC KEY----- + + ''', + 'EthMacAddr': '00:0F:11:58:86:97', + 'FwIdent': '1.6.1+1+WL-1', + 'Hostname': 'PBLR-0000645', + 'HwFixedCableRating': 20, + 'HwFwCompat': 'wlac-2', + 'HwHasBop': True, + 'HwHasBuzzer': True, + 'HwHasEichrechtLaserMarking': False, + 'HwHasEthernet': True, + 'HwHasLed': True, + 'HwHasLte': False, + 'HwHasMeter': True, + 'HwHasMeterDisplay': True, + 'HwHasPlc': False, + 'HwHasRfid': True, + 'HwHasRs485': True, + 'HwHasSocket': False, + 'HwHasTpm': False, + 'HwHasWlan': True, + 'HwMaxCurrent': 16, + 'HwOneOrThreePhase': 3, + 'MainboardPn': '6004-2300-7600', + 'MainboardSn': '23-38-A4E-2MC', + 'MeterCalIGainA': 267369, + 'MeterCalIGainB': 228286, + 'MeterCalIGainC': 246455, + 'MeterCalIRmsOffsetA': 15573, + 'MeterCalIRmsOffsetB': 268422963, + 'MeterCalIRmsOffsetC': 9082, + 'MeterCalPhaseA': 250, + 'MeterCalPhaseB': 271, + 'MeterCalPhaseC': 271, + 'MeterCalVGainA': 250551, + 'MeterCalVGainB': 246074, + 'MeterCalVGainC': 230191, + 'MeterFwIdent': 'b9cbcd', + 'ProductModelName': 'WLAC1-H11R0WE0ICR00', + 'ProductPn': '6004-2300-8002', + 'ProductSn': '23-45-A4O-MOF', + 'ProductVendorName': 'Peblar', + 'WlanApMacAddr': '00:0F:11:58:86:98', + 'WlanStaMacAddr': '00:0F:11:58:86:99', + }), + 'user_configuration': dict({ + 'BopFallbackCurrent': 6000, + 'BopHomeWizardAddress': 'p1meter-093586', + 'BopSource': 'homewizard', + 'BopSourceParameters': '{}', + 'ConnectedPhases': 3, + 'CurrentCtrlBopCtType': 'CTK05-14', + 'CurrentCtrlBopEnable': True, + 'CurrentCtrlBopFuseRating': 35, + 'CurrentCtrlFixedChargeCurrentLimit': 16, + 'GroundMonitoring': True, + 'GroupLoadBalancingEnable': False, + 'GroupLoadBalancingFallbackCurrent': 6, + 'GroupLoadBalancingGroupId': 1, + 'GroupLoadBalancingInterface': 'RS485', + 'GroupLoadBalancingMaxCurrent': 0, + 'GroupLoadBalancingRole': '', + 'HmiBuzzerVolume': 1, + 'HmiLedIntensityManual': 0, + 'HmiLedIntensityMax': 100, + 'HmiLedIntensityMin': 1, + 'HmiLedIntensityMode': 'Fixed', + 'LocalRestApiAccessMode': 'ReadWrite', + 'LocalRestApiAllowed': True, + 'LocalRestApiEnable': True, + 'LocalSmartChargingAllowed': True, + 'ModbusServerAccessMode': 'ReadOnly', + 'ModbusServerAllowed': True, + 'ModbusServerEnable': True, + 'PhaseRotation': 'RST', + 'PowerLimitInputDi1Inverse': False, + 'PowerLimitInputDi1Limit': 6, + 'PowerLimitInputDi2Inverse': False, + 'PowerLimitInputDi2Limit': 0, + 'PowerLimitInputEnable': False, + 'PredefinedCpoName': '', + 'ScheduledChargingAllowed': True, + 'ScheduledChargingEnable': False, + 'SeccOcppActive': False, + 'SeccOcppUri': '', + 'SessionManagerChargeWithoutAuth': False, + 'SolarChargingAllowed': True, + 'SolarChargingEnable': True, + 'SolarChargingMode': 'PureSolar', + 'SolarChargingSource': 'homewizard', + 'SolarChargingSourceParameters': dict({ + 'address': 'p1meter-093586', + }), + 'TimeZone': 'Europe/Amsterdam', + 'UserDefinedChargeLimitCurrent': 16, + 'UserDefinedChargeLimitCurrentAllowed': True, + 'UserDefinedHouseholdPowerLimit': 20000, + 'UserDefinedHouseholdPowerLimitAllowed': True, + 'UserDefinedHouseholdPowerLimitEnable': False, + 'UserDefinedHouseholdPowerLimitSource': 'homewizard', + 'UserKeepSocketLocked': False, + 'VDEPhaseImbalanceEnable': False, + 'VDEPhaseImbalanceLimit': 20, + 'WebIfUpdateHelper': True, + 'smart_charging': 'pure_solar', + }), + 'versions': dict({ + 'available': dict({ + 'Customization': 'Peblar-1.9', + 'Firmware': '1.6.2+1+WL-1', + 'customization_version': '1.9', + 'firmware_version': '1.6.2', + }), + 'current': dict({ + 'Customization': 'Peblar-1.9', + 'Firmware': '1.6.1+1+WL-1', + 'customization_version': '1.9', + 'firmware_version': '1.6.1', + }), + }), + }) +# --- diff --git a/tests/components/peblar/snapshots/test_init.ambr b/tests/components/peblar/snapshots/test_init.ambr new file mode 100644 index 00000000000..8a7cefc523d --- /dev/null +++ b/tests/components/peblar/snapshots/test_init.ambr @@ -0,0 +1,42 @@ +# serializer version: 1 +# name: test_peblar_device_entry + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'http://127.0.0.127', + 'connections': set({ + tuple( + 'mac', + '00:0f:11:58:86:97', + ), + tuple( + 'mac', + '00:0f:11:58:86:99', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'peblar', + '23-45-A4O-MOF', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Peblar', + 'model': 'WLAC1-H11R0WE0ICR00', + 'model_id': '6004-2300-8002', + 'name': 'Peblar EV Charger', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '23-45-A4O-MOF', + 'suggested_area': None, + 'sw_version': '1.6.1+1+WL-1', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/peblar/snapshots/test_number.ambr b/tests/components/peblar/snapshots/test_number.ambr new file mode 100644 index 00000000000..d8e9c756c50 --- /dev/null +++ b/tests/components/peblar/snapshots/test_number.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_entities[number][number.peblar_ev_charger_charge_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 16, + 'min': 6, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.peblar_ev_charger_charge_limit', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge limit', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_current_limit', + 'unique_id': '23-45-A4O-MOF_charge_current_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[number][number.peblar_ev_charger_charge_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Peblar EV Charger Charge limit', + 'max': 16, + 'min': 6, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.peblar_ev_charger_charge_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16', + }) +# --- diff --git a/tests/components/peblar/snapshots/test_select.ambr b/tests/components/peblar/snapshots/test_select.ambr new file mode 100644 index 00000000000..3a600653a84 --- /dev/null +++ b/tests/components/peblar/snapshots/test_select.ambr @@ -0,0 +1,63 @@ +# serializer version: 1 +# name: test_entities[select][select.peblar_ev_charger_smart_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'fast_solar', + 'pure_solar', + 'scheduled', + 'smart_solar', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.peblar_ev_charger_smart_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smart charging', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_charging', + 'unique_id': '23-45-A4O-MOF_smart_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[select][select.peblar_ev_charger_smart_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Peblar EV Charger Smart charging', + 'options': list([ + 'default', + 'fast_solar', + 'pure_solar', + 'scheduled', + 'smart_solar', + ]), + }), + 'context': , + 'entity_id': 'select.peblar_ev_charger_smart_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pure_solar', + }) +# --- diff --git a/tests/components/peblar/snapshots/test_sensor.ambr b/tests/components/peblar/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5a1d1663ba2 --- /dev/null +++ b/tests/components/peblar/snapshots/test_sensor.ambr @@ -0,0 +1,915 @@ +# serializer version: 1 +# name: test_entities[sensor][sensor.peblar_ev_charger_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '23-45-A4O-MOF_current_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Peblar EV Charger Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.242', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_current_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 1', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase_1', + 'unique_id': '23-45-A4O-MOF_current_phase_1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Peblar EV Charger Current phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_current_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.242', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_current_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 2', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase_2', + 'unique_id': '23-45-A4O-MOF_current_phase_2', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Peblar EV Charger Current phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_current_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_current_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 3', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase_3', + 'unique_id': '23-45-A4O-MOF_current_phase_3', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_current_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Peblar EV Charger Current phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_current_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_lifetime_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_lifetime_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lifetime energy', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_total', + 'unique_id': '23-45-A4O-MOF_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_lifetime_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Peblar EV Charger Lifetime energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_lifetime_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '880.703', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_limit_source-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging_cable', + 'current_limiter', + 'dynamic_load_balancing', + 'external_power_limit', + 'group_load_balancing', + 'hardware_limitation', + 'high_temperature', + 'household_power_limit', + 'installation_limit', + 'local_modbus_api', + 'local_rest_api', + 'local_scheduled_charging', + 'ocpp_smart_charging', + 'overcurrent_protection', + 'phase_imbalance', + 'power_factor', + 'solar_charging', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_limit_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Limit source', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_current_limit_source', + 'unique_id': '23-45-A4O-MOF_charge_current_limit_source', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_limit_source-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Peblar EV Charger Limit source', + 'options': list([ + 'charging_cable', + 'current_limiter', + 'dynamic_load_balancing', + 'external_power_limit', + 'group_load_balancing', + 'hardware_limitation', + 'high_temperature', + 'household_power_limit', + 'installation_limit', + 'local_modbus_api', + 'local_rest_api', + 'local_scheduled_charging', + 'ocpp_smart_charging', + 'overcurrent_protection', + 'phase_imbalance', + 'power_factor', + 'solar_charging', + ]), + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_limit_source', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'current_limiter', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.peblar_ev_charger_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '23-45-A4O-MOF_power_total', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Peblar EV Charger Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3185', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power_phase_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_power_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 1', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_phase_1', + 'unique_id': '23-45-A4O-MOF_power_phase_1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power_phase_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Peblar EV Charger Power phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_power_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3185', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_power_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 2', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_phase_2', + 'unique_id': '23-45-A4O-MOF_power_phase_2', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Peblar EV Charger Power phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_power_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_power_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 3', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_phase_3', + 'unique_id': '23-45-A4O-MOF_power_phase_3', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_power_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Peblar EV Charger Power phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_power_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_session_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.peblar_ev_charger_session_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Session energy', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_session', + 'unique_id': '23-45-A4O-MOF_energy_session', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_session_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Peblar EV Charger Session energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_session_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.381', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'suspended', + 'charging', + 'charging', + 'error', + 'fault', + 'invalid', + 'no_ev_connected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.peblar_ev_charger_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cp_state', + 'unique_id': '23-45-A4O-MOF_cp_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Peblar EV Charger State', + 'options': list([ + 'suspended', + 'charging', + 'charging', + 'error', + 'fault', + 'invalid', + 'no_ev_connected', + ]), + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'charging', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'uptime', + 'unique_id': '23-45-A4O-MOF_uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Peblar EV Charger Uptime', + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-12-18T04:16:46+00:00', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_voltage_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 1', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_phase_1', + 'unique_id': '23-45-A4O-MOF_voltage_phase_1', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Peblar EV Charger Voltage phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_voltage_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '223', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_voltage_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 2', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_phase_2', + 'unique_id': '23-45-A4O-MOF_voltage_phase_2', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Peblar EV Charger Voltage phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_voltage_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.peblar_ev_charger_voltage_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 3', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_phase_3', + 'unique_id': '23-45-A4O-MOF_voltage_phase_3', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor][sensor.peblar_ev_charger_voltage_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Peblar EV Charger Voltage phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.peblar_ev_charger_voltage_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/peblar/snapshots/test_switch.ambr b/tests/components/peblar/snapshots/test_switch.ambr new file mode 100644 index 00000000000..46051974339 --- /dev/null +++ b/tests/components/peblar/snapshots/test_switch.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_entities[switch][switch.peblar_ev_charger_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.peblar_ev_charger_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge', + 'unique_id': '23-45-A4O-MOF_charge', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch][switch.peblar_ev_charger_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Peblar EV Charger Charge', + }), + 'context': , + 'entity_id': 'switch.peblar_ev_charger_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entities[switch][switch.peblar_ev_charger_force_single_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.peblar_ev_charger_force_single_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Force single phase', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'force_single_phase', + 'unique_id': '23-45-A4O-MOF_force_single_phase', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch][switch.peblar_ev_charger_force_single_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Peblar EV Charger Force single phase', + }), + 'context': , + 'entity_id': 'switch.peblar_ev_charger_force_single_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/peblar/snapshots/test_update.ambr b/tests/components/peblar/snapshots/test_update.ambr new file mode 100644 index 00000000000..0a6b2bf069f --- /dev/null +++ b/tests/components/peblar/snapshots/test_update.ambr @@ -0,0 +1,120 @@ +# serializer version: 1 +# name: test_entities[update][update.peblar_ev_charger_customization-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.peblar_ev_charger_customization', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Customization', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'customization', + 'unique_id': '23-45-A4O-MOF_customization', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[update][update.peblar_ev_charger_customization-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/peblar/icon.png', + 'friendly_name': 'Peblar EV Charger Customization', + 'in_progress': False, + 'installed_version': 'Peblar-1.9', + 'latest_version': 'Peblar-1.9', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.peblar_ev_charger_customization', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[update][update.peblar_ev_charger_firmware-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'update', + 'entity_category': , + 'entity_id': 'update.peblar_ev_charger_firmware', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Firmware', + 'platform': 'peblar', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '23-45-A4O-MOF_firmware', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[update][update.peblar_ev_charger_firmware-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'device_class': 'firmware', + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/peblar/icon.png', + 'friendly_name': 'Peblar EV Charger Firmware', + 'in_progress': False, + 'installed_version': '1.6.1+1+WL-1', + 'latest_version': '1.6.2+1+WL-1', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.peblar_ev_charger_firmware', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/peblar/test_binary_sensor.py b/tests/components/peblar/test_binary_sensor.py new file mode 100644 index 00000000000..affcde483ea --- /dev/null +++ b/tests/components/peblar/test_binary_sensor.py @@ -0,0 +1,35 @@ +"""Tests for the Peblar binary sensor platform.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("init_integration", [Platform.BINARY_SENSOR], indirect=True) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the binary sensors entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id diff --git a/tests/components/peblar/test_button.py b/tests/components/peblar/test_button.py new file mode 100644 index 00000000000..a47f190a941 --- /dev/null +++ b/tests/components/peblar/test_button.py @@ -0,0 +1,153 @@ +"""Tests for the Peblar button platform.""" + +from unittest.mock import MagicMock + +from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +pytestmark = [ + pytest.mark.freeze_time("2024-12-21 21:45:00"), + pytest.mark.parametrize("init_integration", [Platform.BUTTON], indirect=True), + pytest.mark.usefixtures("init_integration"), +] + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the button entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("entity_id", "method"), + [ + ("button.peblar_ev_charger_identify", "identify"), + ("button.peblar_ev_charger_restart", "reboot"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_buttons( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + method: str, +) -> None: + """Test the Peblar EV charger buttons.""" + mocked_method = getattr(mock_peblar, method) + + # Test normal happy path button press + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 1 + mocked_method.assert_called_with() + + # Test connection error handling + mocked_method.side_effect = PeblarConnectionError("Could not connect") + with pytest.raises( + HomeAssistantError, + match=( + r"An error occurred while communicating " + r"with the Peblar EV charger: Could not connect" + ), + ) as excinfo: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "communication_error" + assert excinfo.value.translation_placeholders == {"error": "Could not connect"} + + # Test unknown error handling + mocked_method.side_effect = PeblarError("Unknown error") + with pytest.raises( + HomeAssistantError, + match=( + r"An unknown error occurred while communicating " + r"with the Peblar EV charger: Unknown error" + ), + ) as excinfo: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "unknown_error" + assert excinfo.value.translation_placeholders == {"error": "Unknown error"} + + # Test authentication error handling + mocked_method.side_effect = PeblarAuthenticationError("Authentication error") + mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") + with pytest.raises( + HomeAssistantError, + match=( + r"An authentication failure occurred while communicating " + r"with the Peblar EV charger" + ), + ) as excinfo: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "authentication_error" + assert not excinfo.value.translation_placeholders + + # Ensure the device is reloaded on authentication error and triggers + # a reauthentication flow. + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/peblar/test_config_flow.py b/tests/components/peblar/test_config_flow.py new file mode 100644 index 00000000000..9f0806f0591 --- /dev/null +++ b/tests/components/peblar/test_config_flow.py @@ -0,0 +1,502 @@ +"""Configuration flow tests for the Peblar integration.""" + +from ipaddress import ip_address +from unittest.mock import MagicMock + +from peblar import PeblarAuthenticationError, PeblarConnectionError +import pytest + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +@pytest.mark.usefixtures("mock_peblar") +async def test_user_flow(hass: HomeAssistant) -> None: + """Test the full happy path user flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.unique_id == "23-45-A4O-MOF" + assert config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + } + assert not config_entry.options + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PeblarConnectionError, {CONF_HOST: "cannot_connect"}), + (PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_user_flow_errors( + hass: HomeAssistant, + mock_peblar: MagicMock, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test we show user form on a connection error.""" + mock_peblar.login.side_effect = side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGCATS!", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == expected_error + + mock_peblar.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "OMGPUPPIES!", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.unique_id == "23-45-A4O-MOF" + assert config_entry.data == { + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "OMGPUPPIES!", + } + assert not config_entry.options + + +@pytest.mark.usefixtures("mock_peblar") +async def test_user_flow_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test configuration flow aborts when the device is already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGSPIDERS", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_peblar") +async def test_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the full happy path reconfigure flow from start to finish.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.127", + CONF_PASSWORD: "OMGSPIDERS", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + } + + +@pytest.mark.usefixtures("mock_peblar") +async def test_reconfigure_to_different_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfiguring to a different device doesn't work.""" + mock_config_entry.add_to_hass(hass) + + # Change the unique ID of the entry, so we have a mismatch + hass.config_entries.async_update_entry(mock_config_entry, unique_id="mismatch") + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "different_device" + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PeblarConnectionError, {CONF_HOST: "cannot_connect"}), + (PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_reconfigure_flow_errors( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test we show user form on a connection error.""" + mock_config_entry.add_to_hass(hass) + mock_peblar.login.side_effect = side_effect + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == expected_error + + mock_peblar.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + assert result["type"] is FlowResultType.ABORT + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "OMGPUPPIES", + } + + +@pytest.mark.usefixtures("mock_peblar") +async def test_zeroconf_flow(hass: HomeAssistant) -> None: + """Test the zeroconf happy flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + port=80, + hostname="pblr-0000645.local.", + name="mock_name", + properties={ + "sn": "23-45-A4O-MOF", + "version": "1.6.1+1+WL-1", + }, + type="mock_type", + ), + ) + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] is FlowResultType.FORM + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + assert progress[0].get("flow_id") == result["flow_id"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "OMGPINEAPPLES"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.unique_id == "23-45-A4O-MOF" + assert config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPINEAPPLES", + } + assert not config_entry.options + + +async def test_zeroconf_flow_abort_no_serial(hass: HomeAssistant) -> None: + """Test the zeroconf aborts when it advertises incompatible data.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + port=80, + hostname="pblr-0000645.local.", + name="mock_name", + properties={}, + type="mock_type", + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_serial_number" + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PeblarConnectionError, {"base": "unknown"}), + (PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_zeroconf_flow_errors( + hass: HomeAssistant, + mock_peblar: MagicMock, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test we show form on a error.""" + mock_peblar.login.side_effect = side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + port=80, + hostname="pblr-0000645.local.", + name="mock_name", + properties={ + "sn": "23-45-A4O-MOF", + "version": "1.6.1+1+WL-1", + }, + type="mock_type", + ), + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["errors"] == expected_error + + mock_peblar.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + config_entry = result["result"] + assert config_entry.unique_id == "23-45-A4O-MOF" + assert config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + } + assert not config_entry.options + + +@pytest.mark.usefixtures("mock_peblar") +async def test_zeroconf_flow_not_discovered_again( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the zeroconf doesn't re-discover an existing device.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + port=80, + hostname="pblr-0000645.local.", + name="mock_name", + properties={ + "sn": "23-45-A4O-MOF", + "version": "1.6.1+1+WL-1", + }, + type="mock_type", + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_peblar") +async def test_user_flow_with_zeroconf_in_progress(hass: HomeAssistant) -> None: + """Test the full happy path user flow from start to finish. + + While zeroconf discovery is already in progress. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("127.0.0.1"), + ip_addresses=[ip_address("127.0.0.1")], + port=80, + hostname="pblr-0000645.local.", + name="mock_name", + properties={ + "sn": "23-45-A4O-MOF", + "version": "1.6.1+1+WL-1", + }, + type="mock_type", + ), + ) + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 2 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + assert not hass.config_entries.flow.async_progress() + + +@pytest.mark.usefixtures("mock_peblar") +async def test_reauth_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the reauthentication configuration flow.""" + mock_config_entry.add_to_hass(hass) + assert mock_config_entry.data[CONF_PASSWORD] == "OMGSPIDERS" + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "OMGPUPPIES"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.127", + CONF_PASSWORD: "OMGPUPPIES", + } + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PeblarConnectionError, {"base": "cannot_connect"}), + (PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test we show form on a error.""" + mock_config_entry.add_to_hass(hass) + mock_peblar.login.side_effect = side_effect + + result = await mock_config_entry.start_reauth_flow(hass) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == expected_error + + mock_peblar.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/peblar/test_coordinator.py b/tests/components/peblar/test_coordinator.py new file mode 100644 index 00000000000..7f073af9554 --- /dev/null +++ b/tests/components/peblar/test_coordinator.py @@ -0,0 +1,119 @@ +"""Tests for the Peblar coordinators.""" + +from datetime import timedelta +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError +import pytest + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, async_fire_time_changed + +pytestmark = [ + pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True), + pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration"), +] + + +@pytest.mark.parametrize( + ("error", "log_message"), + [ + ( + PeblarConnectionError("Could not connect"), + ( + "An error occurred while communicating with the Peblar EV charger: " + "Could not connect" + ), + ), + ( + PeblarError("Unknown error"), + ( + "An unknown error occurred while communicating " + "with the Peblar EV charger: Unknown error" + ), + ), + ], +) +async def test_coordinator_error_handler( + hass: HomeAssistant, + mock_peblar: MagicMock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, + error: Exception, + log_message: str, +) -> None: + """Test the coordinators.""" + entity_id = "sensor.peblar_ev_charger_power" + + # Ensure we are set up and the coordinator is working. + # Confirming this through a sensor entity, that is available. + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE + + # Mock an error in the coordinator. + mock_peblar.rest_api.return_value.meter.side_effect = error + freezer.tick(timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Ensure the sensor entity is now unavailable. + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + # Ensure the error is logged + assert log_message in caplog.text + + # Recover + mock_peblar.rest_api.return_value.meter.side_effect = None + freezer.tick(timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Ensure the sensor entity is now available. + assert (state := hass.states.get("sensor.peblar_ev_charger_power")) + assert state.state != STATE_UNAVAILABLE + + +async def test_coordinator_error_handler_authentication_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the coordinator error handler with an authentication error.""" + + # Ensure the sensor entity is now available. + assert (state := hass.states.get("sensor.peblar_ev_charger_power")) + assert state.state != STATE_UNAVAILABLE + + # Mock an authentication in the coordinator + mock_peblar.rest_api.return_value.meter.side_effect = PeblarAuthenticationError( + "Authentication error" + ) + mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") + freezer.tick(timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Ensure the sensor entity is now unavailable. + assert (state := hass.states.get("sensor.peblar_ev_charger_power")) + assert state.state == STATE_UNAVAILABLE + + # Ensure we have triggered a reauthentication flow + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/peblar/test_diagnostics.py b/tests/components/peblar/test_diagnostics.py new file mode 100644 index 00000000000..11f9af28b2d --- /dev/null +++ b/tests/components/peblar/test_diagnostics.py @@ -0,0 +1,22 @@ +"""Tests for the diagnostics data provided by the Peblar integration.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) diff --git a/tests/components/peblar/test_init.py b/tests/components/peblar/test_init.py new file mode 100644 index 00000000000..6e6a9c2af05 --- /dev/null +++ b/tests/components/peblar/test_init.py @@ -0,0 +1,96 @@ +"""Integration tests for the Peblar integration.""" + +from unittest.mock import MagicMock + +from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, +) -> None: + """Test the Peblar configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert len(mock_peblar.login.mock_calls) == 1 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "exception", + [PeblarConnectionError, PeblarError], +) +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, + exception: Exception, +) -> None: + """Test the Peblar configuration entry not ready.""" + mock_peblar.login.side_effect = exception + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_peblar.login.mock_calls) == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_authentication_failed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, +) -> None: + """Test authentication error, aborts setup.""" + mock_config_entry.add_to_hass(hass) + + mock_peblar.login.side_effect = PeblarAuthenticationError + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id + + +@pytest.mark.usefixtures("init_integration") +async def test_peblar_device_entry( + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test authentication error, aborts setup.""" + assert ( + device_entry := device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + ) + assert device_entry == snapshot diff --git a/tests/components/peblar/test_number.py b/tests/components/peblar/test_number.py new file mode 100644 index 00000000000..fa49b6ab116 --- /dev/null +++ b/tests/components/peblar/test_number.py @@ -0,0 +1,267 @@ +"""Tests for the Peblar number platform.""" + +from unittest.mock import MagicMock + +from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import ( + MockConfigEntry, + mock_restore_cache_with_extra_data, + snapshot_platform, +) + + +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the number entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration", "entity_registry_enabled_by_default") +async def test_number_set_value( + hass: HomeAssistant, + mock_peblar: MagicMock, +) -> None: + """Test the Peblar EV charger numbers.""" + entity_id = "number.peblar_ev_charger_charge_limit" + mocked_method = mock_peblar.rest_api.return_value.ev_interface + mocked_method.reset_mock() + + # Test normal happy path number value change + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 2 + mocked_method.mock_calls[0].assert_called_with({"charge_current_limit": 10}) + + +async def test_number_set_value_when_charging_is_suspended( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test handling of setting the charging limit while charging is suspended.""" + entity_id = "number.peblar_ev_charger_charge_limit" + + # Suspend charging + mock_peblar.rest_api.return_value.ev_interface.return_value.charge_current_limit = 0 + + # Setup the config entry + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mocked_method = mock_peblar.rest_api.return_value.ev_interface + mocked_method.reset_mock() + + # Test normal happy path number value change + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 0 + + # Check the state is reflected + assert (state := hass.states.get(entity_id)) + assert state.state == "10" + + +@pytest.mark.parametrize( + ("error", "error_match", "translation_key", "translation_placeholders"), + [ + ( + PeblarConnectionError("Could not connect"), + ( + r"An error occurred while communicating " + r"with the Peblar EV charger: Could not connect" + ), + "communication_error", + {"error": "Could not connect"}, + ), + ( + PeblarError("Unknown error"), + ( + r"An unknown error occurred while communicating " + r"with the Peblar EV charger: Unknown error" + ), + "unknown_error", + {"error": "Unknown error"}, + ), + ], +) +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration", "entity_registry_enabled_by_default") +async def test_number_set_value_communication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + error: Exception, + error_match: str, + translation_key: str, + translation_placeholders: dict, +) -> None: + """Test the Peblar EV charger when a communication error occurs.""" + entity_id = "number.peblar_ev_charger_charge_limit" + mock_peblar.rest_api.return_value.ev_interface.side_effect = error + + with pytest.raises( + HomeAssistantError, + match=error_match, + ) as excinfo: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == translation_key + assert excinfo.value.translation_placeholders == translation_placeholders + + +@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) +@pytest.mark.usefixtures("init_integration") +async def test_number_set_value_authentication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Peblar EV charger when an authentication error occurs.""" + entity_id = "number.peblar_ev_charger_charge_limit" + mock_peblar.rest_api.return_value.ev_interface.side_effect = ( + PeblarAuthenticationError("Authentication error") + ) + mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") + with pytest.raises( + HomeAssistantError, + match=( + r"An authentication failure occurred while communicating " + r"with the Peblar EV charger" + ), + ) as excinfo: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "authentication_error" + assert not excinfo.value.translation_placeholders + + # Ensure the device is reloaded on authentication error and triggers + # a reauthentication flow. + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id + + +@pytest.mark.parametrize( + ("restore_state", "restore_native_value", "expected_state"), + [ + ("10", 10, "10"), + ("unknown", 10, "unknown"), + ("unavailable", 10, "unknown"), + ("10", None, "unknown"), + ], +) +async def test_restore_state( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + restore_state: str, + restore_native_value: int, + expected_state: str, +) -> None: + """Test restoring the number state.""" + EXTRA_STORED_DATA = { + "native_max_value": 16, + "native_min_value": 6, + "native_step": 1, + "native_unit_of_measurement": "A", + "native_value": restore_native_value, + } + mock_restore_cache_with_extra_data( + hass, + ( + ( + State("number.peblar_ev_charger_charge_limit", restore_state), + EXTRA_STORED_DATA, + ), + ), + ) + + # Adjust Peblar client to have an ignored value for the charging limit + mock_peblar.rest_api.return_value.ev_interface.return_value.charge_current_limit = 0 + + # Setup the config entry + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Check if state is restored and value is set correctly + assert (state := hass.states.get("number.peblar_ev_charger_charge_limit")) + assert state.state == expected_state diff --git a/tests/components/peblar/test_select.py b/tests/components/peblar/test_select.py new file mode 100644 index 00000000000..be7e182dc39 --- /dev/null +++ b/tests/components/peblar/test_select.py @@ -0,0 +1,184 @@ +"""Tests for the Peblar select platform.""" + +from unittest.mock import MagicMock + +from peblar import ( + PeblarAuthenticationError, + PeblarConnectionError, + PeblarError, + SmartChargingMode, +) +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +pytestmark = [ + pytest.mark.parametrize("init_integration", [Platform.SELECT], indirect=True), + pytest.mark.usefixtures("init_integration"), +] + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the select entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_option( + hass: HomeAssistant, + mock_peblar: MagicMock, +) -> None: + """Test the Peblar EV charger selects.""" + entity_id = "select.peblar_ev_charger_smart_charging" + mocked_method = mock_peblar.smart_charging + mocked_method.reset_mock() + + # Test normal happy path for changing the select option + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "default", + }, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 1 + mocked_method.assert_called_with(SmartChargingMode.DEFAULT) + + +@pytest.mark.parametrize( + ("error", "error_match", "translation_key", "translation_placeholders"), + [ + ( + PeblarConnectionError("Could not connect"), + ( + r"An error occurred while communicating " + r"with the Peblar EV charger: Could not connect" + ), + "communication_error", + {"error": "Could not connect"}, + ), + ( + PeblarError("Unknown error"), + ( + r"An unknown error occurred while communicating " + r"with the Peblar EV charger: Unknown error" + ), + "unknown_error", + {"error": "Unknown error"}, + ), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_option_communication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + error: Exception, + error_match: str, + translation_key: str, + translation_placeholders: dict, +) -> None: + """Test the Peblar EV charger when a communication error occurs.""" + entity_id = "select.peblar_ev_charger_smart_charging" + mock_peblar.smart_charging.side_effect = error + + with pytest.raises( + HomeAssistantError, + match=error_match, + ) as excinfo: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "default", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == translation_key + assert excinfo.value.translation_placeholders == translation_placeholders + + +async def test_select_option_authentication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Peblar EV charger when an authentication error occurs.""" + entity_id = "select.peblar_ev_charger_smart_charging" + mock_peblar.smart_charging.side_effect = PeblarAuthenticationError( + "Authentication error" + ) + mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") + + with pytest.raises( + HomeAssistantError, + match=( + r"An authentication failure occurred while communicating " + r"with the Peblar EV charger" + ), + ) as excinfo: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "default", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "authentication_error" + assert not excinfo.value.translation_placeholders + + # Ensure the device is reloaded on authentication error and triggers + # a reauthentication flow. + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/peblar/test_sensor.py b/tests/components/peblar/test_sensor.py new file mode 100644 index 00000000000..d689e66e944 --- /dev/null +++ b/tests/components/peblar/test_sensor.py @@ -0,0 +1,36 @@ +"""Tests for the Peblar sensor platform.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.freeze_time("2024-12-21 21:45:00") +@pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True) +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the sensor entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id diff --git a/tests/components/peblar/test_switch.py b/tests/components/peblar/test_switch.py new file mode 100644 index 00000000000..a7dab51eb3a --- /dev/null +++ b/tests/components/peblar/test_switch.py @@ -0,0 +1,204 @@ +"""Tests for the Peblar switch platform.""" + +from unittest.mock import MagicMock + +from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +pytestmark = [ + pytest.mark.parametrize("init_integration", [Platform.SWITCH], indirect=True), + pytest.mark.usefixtures("init_integration"), +] + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id + + +@pytest.mark.parametrize( + ("service", "entity_id", "parameter", "parameter_value"), + [ + ( + SERVICE_TURN_ON, + "switch.peblar_ev_charger_force_single_phase", + "force_single_phase", + True, + ), + ( + SERVICE_TURN_OFF, + "switch.peblar_ev_charger_force_single_phase", + "force_single_phase", + False, + ), + ( + SERVICE_TURN_ON, + "switch.peblar_ev_charger_charge", + "charge_current_limit", + 16, + ), + ( + SERVICE_TURN_OFF, + "switch.peblar_ev_charger_charge", + "charge_current_limit", + 0, + ), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_switch( + hass: HomeAssistant, + mock_peblar: MagicMock, + service: str, + entity_id: str, + parameter: str, + parameter_value: bool | int, +) -> None: + """Test the Peblar EV charger switches.""" + mocked_method = mock_peblar.rest_api.return_value.ev_interface + mocked_method.reset_mock() + + # Test normal happy path for changing the switch state + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 2 + mocked_method.mock_calls[0].assert_called_with({parameter: parameter_value}) + + +@pytest.mark.parametrize( + ("error", "error_match", "translation_key", "translation_placeholders"), + [ + ( + PeblarConnectionError("Could not connect"), + ( + r"An error occurred while communicating " + r"with the Peblar EV charger: Could not connect" + ), + "communication_error", + {"error": "Could not connect"}, + ), + ( + PeblarError("Unknown error"), + ( + r"An unknown error occurred while communicating " + r"with the Peblar EV charger: Unknown error" + ), + "unknown_error", + {"error": "Unknown error"}, + ), + ], +) +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_switch_communication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + error: Exception, + error_match: str, + translation_key: str, + translation_placeholders: dict, + service: str, +) -> None: + """Test the Peblar EV charger when a communication error occurs.""" + entity_id = "switch.peblar_ev_charger_force_single_phase" + mock_peblar.rest_api.return_value.ev_interface.side_effect = error + with pytest.raises( + HomeAssistantError, + match=error_match, + ) as excinfo: + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == translation_key + assert excinfo.value.translation_placeholders == translation_placeholders + + +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +async def test_switch_authentication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + service: str, +) -> None: + """Test the Peblar EV charger when an authentication error occurs.""" + entity_id = "switch.peblar_ev_charger_force_single_phase" + mock_peblar.rest_api.return_value.ev_interface.side_effect = ( + PeblarAuthenticationError("Authentication error") + ) + mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") + + with pytest.raises( + HomeAssistantError, + match=( + r"An authentication failure occurred while communicating " + r"with the Peblar EV charger" + ), + ) as excinfo: + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "authentication_error" + assert not excinfo.value.translation_placeholders + + # Ensure the device is reloaded on authentication error and triggers + # a reauthentication flow. + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id diff --git a/tests/components/peblar/test_update.py b/tests/components/peblar/test_update.py new file mode 100644 index 00000000000..54eb77abc24 --- /dev/null +++ b/tests/components/peblar/test_update.py @@ -0,0 +1,35 @@ +"""Tests for the Peblar update platform.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.peblar.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("init_integration", [Platform.UPDATE], indirect=True) +@pytest.mark.usefixtures("init_integration") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the update entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + # Ensure all entities are correctly assigned to the Peblar EV charger + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "23-45-A4O-MOF")} + ) + assert device_entry + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + for entity_entry in entity_entries: + assert entity_entry.device_id == device_entry.id diff --git a/tests/components/pegel_online/test_init.py b/tests/components/pegel_online/test_init.py index c1b8f1861c4..ac153193983 100644 --- a/tests/components/pegel_online/test_init.py +++ b/tests/components/pegel_online/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.pegel_online.const import ( DOMAIN, MIN_TIME_BETWEEN_UPDATES, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.util import utcnow @@ -24,6 +25,27 @@ from .const import ( from tests.common import MockConfigEntry, async_fire_time_changed +async def test_setup_error( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Tests error during config entry setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG_ENTRY_DATA_DRESDEN, + unique_id=MOCK_CONFIG_ENTRY_DATA_DRESDEN[CONF_STATION], + ) + entry.add_to_hass(hass) + with patch("homeassistant.components.pegel_online.PegelOnline") as pegelonline: + pegelonline.return_value = PegelOnlineMock( + station_details=MOCK_STATION_DETAILS_DRESDEN, + station_measurements=MOCK_STATION_MEASUREMENT_DRESDEN, + ) + pegelonline().override_side_effect(ClientError("Boom")) + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state is ConfigEntryState.SETUP_RETRY + + async def test_update_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/components/persistent_notification/conftest.py b/tests/components/persistent_notification/conftest.py index 29ba5a6008a..76fdc70ea7b 100644 --- a/tests/components/persistent_notification/conftest.py +++ b/tests/components/persistent_notification/conftest.py @@ -2,7 +2,7 @@ import pytest -import homeassistant.components.persistent_notification as pn +from homeassistant.components import persistent_notification as pn from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 956183d8420..89559d45dc4 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,6 +1,6 @@ """The tests for the persistent notification component.""" -import homeassistant.components.persistent_notification as pn +from homeassistant.components import persistent_notification as pn from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/persistent_notification/test_trigger.py b/tests/components/persistent_notification/test_trigger.py index 16208143447..5e03fbf5f19 100644 --- a/tests/components/persistent_notification/test_trigger.py +++ b/tests/components/persistent_notification/test_trigger.py @@ -2,7 +2,7 @@ from typing import Any -import homeassistant.components.persistent_notification as pn +from homeassistant.components import persistent_notification as pn from homeassistant.components.persistent_notification import trigger from homeassistant.core import Context, HomeAssistant, callback diff --git a/tests/components/pglab/__init__.py b/tests/components/pglab/__init__.py new file mode 100644 index 00000000000..0ee9b203524 --- /dev/null +++ b/tests/components/pglab/__init__.py @@ -0,0 +1 @@ +"""Tests for the PG LAB Electronics integration.""" diff --git a/tests/components/pglab/conftest.py b/tests/components/pglab/conftest.py new file mode 100644 index 00000000000..b148cb08a15 --- /dev/null +++ b/tests/components/pglab/conftest.py @@ -0,0 +1,41 @@ +"""Common fixtures for the PG LAB Electronics tests.""" + +import pytest + +from homeassistant.components.pglab.const import DISCOVERY_TOPIC, DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, mock_device_registry, mock_registry + +CONF_DISCOVERY_PREFIX = "discovery_prefix" + + +@pytest.fixture +def device_reg(hass: HomeAssistant): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass: HomeAssistant): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +async def setup_pglab(hass: HomeAssistant): + """Set up PG LAB Electronics.""" + hass.config.components.add("pglab") + + entry = MockConfigEntry( + data={CONF_DISCOVERY_PREFIX: DISCOVERY_TOPIC}, + domain=DOMAIN, + title="PG LAB Electronics", + ) + + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "pglab" in hass.config.components diff --git a/tests/components/pglab/test_config_flow.py b/tests/components/pglab/test_config_flow.py new file mode 100644 index 00000000000..81ed010920e --- /dev/null +++ b/tests/components/pglab/test_config_flow.py @@ -0,0 +1,133 @@ +"""Test the PG LAB Electronics config flow.""" + +from homeassistant.components.mqtt import MQTT_CONNECTION_STATE +from homeassistant.components.pglab.const import DOMAIN +from homeassistant.config_entries import SOURCE_MQTT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo + +from tests.common import MockConfigEntry +from tests.typing import MqttMockHAClient + + +async def test_mqtt_config_single_instance( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient +) -> None: + """Test MQTT flow aborts when an entry already exist.""" + + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT} + ) + + # Be sure that result is abort. Only single instance is allowed. + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: + """Test we can finish a config flow through MQTT with custom prefix.""" + discovery_info = MqttServiceInfo( + topic="pglab/discovery/E-Board-DD53AC85/config", + payload=( + '{"ip":"192.168.1.16", "mac":"80:34:28:1B:18:5A", "name":"e-board-office",' + '"hw":"255.255.255", "fw":"255.255.255", "type":"E-Board", "id":"E-Board-DD53AC85",' + '"manufacturer":"PG LAB Electronics", "params":{"shutters":0, "boards":"10000000" } }' + ), + qos=0, + retain=False, + subscribed_topic="pglab/discovery/#", + timestamp=None, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery_info + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["result"].data == {"discovery_prefix": "pglab/discovery"} + + +async def test_mqtt_abort_invalid_topic( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient +) -> None: + """Check MQTT flow aborts if discovery topic is invalid.""" + discovery_info = MqttServiceInfo( + topic="pglab/discovery/E-Board-DD53AC85/wrong_topic", + payload=( + '{"ip":"192.168.1.16", "mac":"80:34:28:1B:18:5A", "name":"e-board-office",' + '"hw":"255.255.255", "fw":"255.255.255", "type":"E-Board", "id":"E-Board-DD53AC85",' + '"manufacturer":"PG LAB Electronics", "params":{"shutters":0, "boards":"10000000" } }' + ), + qos=0, + retain=False, + subscribed_topic="pglab/discovery/#", + timestamp=None, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery_info + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "invalid_discovery_info" + + discovery_info = MqttServiceInfo( + topic="pglab/discovery/E-Board-DD53AC85/config", + payload="", + qos=0, + retain=False, + subscribed_topic="pglab/discovery/#", + timestamp=None, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery_info + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "invalid_discovery_info" + + +async def test_user_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: + """Test if the user can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].data == { + "discovery_prefix": "pglab/discovery", + } + + +async def test_user_setup_mqtt_not_connected( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient +) -> None: + """Test that the user setup is aborted when MQTT is not connected.""" + + mqtt_mock.connected = False + async_dispatcher_send(hass, MQTT_CONNECTION_STATE, False) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "mqtt_not_connected" + + +async def test_user_setup_mqtt_not_configured(hass: HomeAssistant) -> None: + """Test that the user setup is aborted when MQTT is not configured.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "mqtt_not_configured" diff --git a/tests/components/pglab/test_discovery.py b/tests/components/pglab/test_discovery.py new file mode 100644 index 00000000000..65716236277 --- /dev/null +++ b/tests/components/pglab/test_discovery.py @@ -0,0 +1,154 @@ +"""The tests for the PG LAB Electronics discovery device.""" + +import json + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import async_fire_mqtt_message +from tests.typing import MqttMockHAClient + + +async def test_device_discover( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + device_reg, + entity_reg, + setup_pglab, +) -> None: + """Test setting up a device.""" + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "11000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Verify device and registry entries are created + device_entry = device_reg.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])} + ) + assert device_entry is not None + assert device_entry.configuration_url == f"http://{payload['ip']}/" + assert device_entry.manufacturer == "PG LAB Electronics" + assert device_entry.model == payload["type"] + assert device_entry.name == payload["name"] + assert device_entry.sw_version == payload["fw"] + + +async def test_device_update( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + device_reg, + entity_reg, + setup_pglab, + snapshot: SnapshotAssertion, +) -> None: + """Test update a device.""" + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "11000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Verify device is created + device_entry = device_reg.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])} + ) + assert device_entry is not None + + # update device + payload["fw"] = "1.0.1" + payload["hw"] = "1.0.8" + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Verify device is created + device_entry = device_reg.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])} + ) + assert device_entry is not None + assert device_entry.sw_version == "1.0.1" + assert device_entry.hw_version == "1.0.8" + + +async def test_device_remove( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + device_reg, + entity_reg, + setup_pglab, +) -> None: + """Test remove a device.""" + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "11000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Verify device is created + device_entry = device_reg.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])} + ) + assert device_entry is not None + + async_fire_mqtt_message( + hass, + topic, + "", + ) + await hass.async_block_till_done() + + # Verify device entry is removed + device_entry = device_reg.async_get_device( + connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])} + ) + assert device_entry is None diff --git a/tests/components/pglab/test_init.py b/tests/components/pglab/test_init.py new file mode 100644 index 00000000000..a6353054e8c --- /dev/null +++ b/tests/components/pglab/test_init.py @@ -0,0 +1 @@ +"""Test the PG LAB Electronics integration.""" diff --git a/tests/components/pglab/test_switch.py b/tests/components/pglab/test_switch.py new file mode 100644 index 00000000000..fef445f80f3 --- /dev/null +++ b/tests/components/pglab/test_switch.py @@ -0,0 +1,318 @@ +"""The tests for the PG LAB Electronics switch.""" + +from datetime import timedelta +import json + +from homeassistant import config_entries +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_mqtt_message, async_fire_time_changed +from tests.typing import MqttMockHAClient + + +async def call_service(hass: HomeAssistant, entity_id, service, **kwargs): + """Call a service.""" + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id, **kwargs}, + blocking=True, + ) + + +async def test_available_relay( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab +) -> None: + """Check if relay are properly created when two E-Relay boards are connected.""" + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "11000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + for i in range(16): + state = hass.states.get(f"switch.test_relay_{i}") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_change_state_via_mqtt( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab +) -> None: + """Test state update via MQTT.""" + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "10000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Simulate response from the device + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Turn relay OFF + async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "OFF") + await hass.async_block_till_done() + state = hass.states.get("switch.test_relay_0") + assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert state.state == STATE_OFF + + # Turn relay ON + async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON") + await hass.async_block_till_done() + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_ON + + # Turn relay OFF + async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "OFF") + await hass.async_block_till_done() + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_OFF + + # Turn relay ON + async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON") + await hass.async_block_till_done() + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_ON + + +async def test_mqtt_state_by_calling_service( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab +) -> None: + """Calling service to turn ON/OFF relay and check mqtt state.""" + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "10000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Turn relay ON + await call_service(hass, "switch.test_relay_0", SERVICE_TURN_ON) + mqtt_mock.async_publish.assert_called_once_with( + "pglab/test/relay/0/set", "ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn relay OFF + await call_service(hass, "switch.test_relay_0", SERVICE_TURN_OFF) + mqtt_mock.async_publish.assert_called_once_with( + "pglab/test/relay/0/set", "OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn relay ON + await call_service(hass, "switch.test_relay_3", SERVICE_TURN_ON) + mqtt_mock.async_publish.assert_called_once_with( + "pglab/test/relay/3/set", "ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn relay OFF + await call_service(hass, "switch.test_relay_3", SERVICE_TURN_OFF) + mqtt_mock.async_publish.assert_called_once_with( + "pglab/test/relay/3/set", "OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_discovery_update( + hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab +) -> None: + """Update discovery message and check if relay are property updated.""" + + # publish the first discovery message + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "first_test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "10000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # test the available relay in the first configuration + for i in range(8): + state = hass.states.get(f"switch.first_test_relay_{i}") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # prepare a new message ... the same device but renamed + # and with different relay configuration + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "second_test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "11000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # be sure that old relay are been removed + for i in range(8): + assert not hass.states.get(f"switch.first_test_relay_{i}") + + # check new relay + for i in range(16): + state = hass.states.get(f"switch.second_test_relay_{i}") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_disable_entity_state_change_via_mqtt( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mqtt_mock: MqttMockHAClient, + setup_pglab, +) -> None: + """Test state update via MQTT of disable entity.""" + + topic = "pglab/discovery/E-Board-DD53AC85/config" + payload = { + "ip": "192.168.1.16", + "mac": "80:34:28:1B:18:5A", + "name": "test", + "hw": "1.0.7", + "fw": "1.0.0", + "type": "E-Board", + "id": "E-Board-DD53AC85", + "manufacturer": "PG LAB Electronics", + "params": {"shutters": 0, "boards": "10000000"}, + } + + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Be sure that the entity relay_0 is available + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Disable entity relay_0 + new_status = entity_registry.async_update_entity( + "switch.test_relay_0", disabled_by=er.RegistryEntryDisabler.USER + ) + + # Be sure that the entity is disabled + assert new_status.disabled is True + + # Try to change the state of the disabled relay_0 + async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON") + await hass.async_block_till_done() + + # Enable entity relay_0 + new_status = entity_registry.async_update_entity( + "switch.test_relay_0", disabled_by=None + ) + + # Be sure that the entity is enabled + assert new_status.disabled is False + + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=config_entries.RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + # Re-send the discovery message + async_fire_mqtt_message( + hass, + topic, + json.dumps(payload), + ) + await hass.async_block_till_done() + + # Be sure that the state is not changed + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_UNKNOWN + + # Try again to change the state of the disabled relay_0 + async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON") + await hass.async_block_till_done() + + # Be sure that the state is been updated + state = hass.states.get("switch.test_relay_0") + assert state.state == STATE_ON diff --git a/tests/components/ping/snapshots/test_binary_sensor.ambr b/tests/components/ping/snapshots/test_binary_sensor.ambr index 0196c2cbbfb..bb28432841f 100644 --- a/tests/components/ping/snapshots/test_binary_sensor.ambr +++ b/tests/components/ping/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ping/snapshots/test_sensor.ambr b/tests/components/ping/snapshots/test_sensor.ambr index d1548f7559c..bb811af6a34 100644 --- a/tests/components/ping/snapshots/test_sensor.ambr +++ b/tests/components/ping/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -114,6 +116,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ping/test_helpers.py b/tests/components/ping/test_helpers.py new file mode 100644 index 00000000000..5a90c6b75b2 --- /dev/null +++ b/tests/components/ping/test_helpers.py @@ -0,0 +1,59 @@ +"""Test the exception handling in subprocess version of async_ping.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.ping.helpers import PingDataSubProcess +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +class MockAsyncSubprocess: + """Minimal mock implementation of asyncio.subprocess.Process for exception testing.""" + + def __init__(self, killsig=ProcessLookupError, **kwargs) -> None: + """Store provided exception type for later.""" + self.killsig = killsig + + async def communicate(self) -> None: + """Fails immediately with a timeout.""" + raise TimeoutError + + async def kill(self) -> None: + """Raise preset exception when called.""" + raise self.killsig + + +@pytest.mark.parametrize("exc", [TypeError, ProcessLookupError]) +async def test_async_ping_expected_exceptions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + exc: Exception, +) -> None: + """Test PingDataSubProcess.async_ping handles expected exceptions.""" + with patch( + "asyncio.create_subprocess_exec", return_value=MockAsyncSubprocess(killsig=exc) + ): + # Actual parameters irrelevant, as subprocess will not be created + ping = PingDataSubProcess(hass, host="10.10.10.10", count=3, privileged=False) + assert await ping.async_ping() is None + + +async def test_async_ping_unexpected_exceptions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test PingDataSubProcess.async_ping does not suppress unexpected exceptions.""" + with patch( + "asyncio.create_subprocess_exec", + return_value=MockAsyncSubprocess(killsig=KeyboardInterrupt), + ): + # Actual parameters irrelevant, as subprocess will not be created + ping = PingDataSubProcess(hass, host="10.10.10.10", count=3, privileged=False) + with pytest.raises(KeyboardInterrupt): + assert await ping.async_ping() is None diff --git a/tests/components/plaato/snapshots/test_binary_sensor.ambr b/tests/components/plaato/snapshots/test_binary_sensor.ambr index e8db3bf32d8..76c0a299c5e 100644 --- a/tests/components/plaato/snapshots/test_binary_sensor.ambr +++ b/tests/components/plaato/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/plaato/snapshots/test_sensor.ambr b/tests/components/plaato/snapshots/test_sensor.ambr index 110ffb04ba9..24ba62e28ca 100644 --- a/tests/components/plaato/snapshots/test_sensor.ambr +++ b/tests/components/plaato/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -146,6 +149,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -193,6 +197,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -239,6 +244,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -286,6 +292,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -333,6 +340,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -379,6 +387,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -428,6 +437,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -478,6 +488,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -528,6 +539,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index 434c31996e4..4dc80d3e7aa 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -7,7 +7,7 @@ from plexwebsocket import SIGNAL_CONNECTION_STATE, STATE_CONNECTED from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import UNDEFINED, UndefinedType -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index c4ec108bb6b..42dcf449168 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -523,7 +523,7 @@ async def test_callback_view( assert result["type"] is FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() - forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' + forward_url = f"{config_flow.AUTH_CALLBACK_PATH}?flow_id={result['flow_id']}" resp = await client.get(forward_url) assert resp.status == HTTPStatus.OK diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 490091998ff..036b2d87f3f 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -25,7 +25,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL from .helpers import trigger_plex_update, wait_for_debouncer diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index dead58e0581..e0a61106101 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -8,7 +8,6 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from packaging.version import Version -from plugwise import PlugwiseData import pytest from homeassistant.components.plugwise.const import DOMAIN @@ -30,6 +29,51 @@ def _read_json(environment: str, call: str) -> dict[str, Any]: return json.loads(fixture) +@pytest.fixture +def cooling_present(request: pytest.FixtureRequest) -> str: + """Pass the cooling_present boolean. + + Used with fixtures that require parametrization of the cooling capability. + """ + return request.param + + +@pytest.fixture +def chosen_env(request: pytest.FixtureRequest) -> str: + """Pass the chosen_env string. + + Used with fixtures that require parametrization of the user-data fixture. + """ + return request.param + + +@pytest.fixture +def gateway_id(request: pytest.FixtureRequest) -> str: + """Pass the gateway_id string. + + Used with fixtures that require parametrization of the gateway_id. + """ + return request.param + + +@pytest.fixture +def heater_id(request: pytest.FixtureRequest) -> str: + """Pass the heater_idstring. + + Used with fixtures that require parametrization of the heater_id. + """ + return request.param + + +@pytest.fixture +def reboot(request: pytest.FixtureRequest) -> str: + """Pass the reboot boolean. + + Used with fixtures that require parametrization of the reboot capability. + """ + return request.param + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" @@ -64,11 +108,14 @@ def mock_smile_config_flow() -> Generator[MagicMock]: autospec=True, ) as smile_mock: smile = smile_mock.return_value + + smile.connect.return_value = Version("4.3.2") smile.smile_hostname = "smile12345" smile.smile_model = "Test Model" smile.smile_model_id = "Test Model ID" smile.smile_name = "Test Smile Name" - smile.connect.return_value = Version("4.3.2") + smile.smile_version = "4.3.2" + yield smile @@ -76,236 +123,132 @@ def mock_smile_config_flow() -> Generator[MagicMock]: def mock_smile_adam() -> Generator[MagicMock]: """Create a Mock Adam environment for testing exceptions.""" chosen_env = "m_adam_multiple_devices_per_zone" - - with patch( - "homeassistant.components.plugwise.coordinator.Smile", autospec=True - ) as smile_mock: + data = _read_json(chosen_env, "data") + with ( + patch( + "homeassistant.components.plugwise.coordinator.Smile", autospec=True + ) as smile_mock, + patch( + "homeassistant.components.plugwise.config_flow.Smile", + new=smile_mock, + ), + ): smile = smile_mock.return_value + smile.async_update.return_value = data + smile.cooling_present = False + smile.connect.return_value = Version("3.0.15") smile.gateway_id = "fe799307f1624099878210aa0b9f1475" smile.heater_id = "90986d591dcd426cae3ec3e8111ff730" + smile.reboot = True + smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" + smile.smile_model_id = "smile_open_therm" + smile.smile_name = "Adam" + smile.smile_type = "thermostat" smile.smile_version = "3.0.15" - smile.smile_type = "thermostat" - smile.smile_hostname = "smile98765" - smile.smile_model = "Gateway" - smile.smile_model_id = "smile_open_therm" - smile.smile_name = "Adam" - smile.connect.return_value = Version("3.0.15") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) yield smile @pytest.fixture -def mock_smile_adam_2() -> Generator[MagicMock]: - """Create a 2nd Mock Adam environment for testing exceptions.""" - chosen_env = "m_adam_heating" - +def mock_smile_adam_heat_cool( + chosen_env: str, cooling_present: bool +) -> Generator[MagicMock]: + """Create a special base Mock Adam type for testing with different datasets.""" + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("3.6.4") + smile.cooling_present = cooling_present smile.gateway_id = "da224107914542988a88561b4452b0f6" smile.heater_id = "056ee145a816487eaa69243c3280f8bf" - smile.smile_version = "3.6.4" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_open_therm" smile.smile_name = "Adam" - smile.connect.return_value = Version("3.6.4") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "thermostat" + smile.smile_version = "3.6.4" yield smile @pytest.fixture -def mock_smile_adam_3() -> Generator[MagicMock]: - """Create a 3rd Mock Adam environment for testing exceptions.""" - chosen_env = "m_adam_cooling" - - with patch( - "homeassistant.components.plugwise.coordinator.Smile", autospec=True - ) as smile_mock: - smile = smile_mock.return_value - - smile.gateway_id = "da224107914542988a88561b4452b0f6" - smile.heater_id = "056ee145a816487eaa69243c3280f8bf" - smile.smile_version = "3.6.4" - smile.smile_type = "thermostat" - smile.smile_hostname = "smile98765" - smile.smile_model = "Gateway" - smile.smile_model_id = "smile_open_therm" - smile.smile_name = "Adam" - smile.connect.return_value = Version("3.6.4") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) - - yield smile - - -@pytest.fixture -def mock_smile_adam_4() -> Generator[MagicMock]: - """Create a 4th Mock Adam environment for testing exceptions.""" +def mock_smile_adam_jip() -> Generator[MagicMock]: + """Create a Mock adam-jip type for testing exceptions.""" chosen_env = "m_adam_jip" - + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("3.2.8") + smile.cooling_present = False smile.gateway_id = "b5c2386c6f6342669e50fe49dd05b188" smile.heater_id = "e4684553153b44afbef2200885f379dc" - smile.smile_version = "3.2.8" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_open_therm" smile.smile_name = "Adam" - smile.connect.return_value = Version("3.2.8") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "thermostat" + smile.smile_version = "3.2.8" yield smile @pytest.fixture -def mock_smile_anna() -> Generator[MagicMock]: - """Create a Mock Anna environment for testing exceptions.""" - chosen_env = "anna_heatpump_heating" +def mock_smile_anna(chosen_env: str, cooling_present: bool) -> Generator[MagicMock]: + """Create a Mock Anna type for testing.""" + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("4.0.15") + smile.cooling_present = cooling_present smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" - smile.smile_version = "4.0.15" - smile.smile_type = "thermostat" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile_thermo" smile.smile_name = "Smile Anna" - smile.connect.return_value = Version("4.0.15") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) - - yield smile - - -@pytest.fixture -def mock_smile_anna_2() -> Generator[MagicMock]: - """Create a 2nd Mock Anna environment for testing exceptions.""" - chosen_env = "m_anna_heatpump_cooling" - with patch( - "homeassistant.components.plugwise.coordinator.Smile", autospec=True - ) as smile_mock: - smile = smile_mock.return_value - - smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" - smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" - smile.smile_version = "4.0.15" smile.smile_type = "thermostat" - smile.smile_hostname = "smile98765" - smile.smile_model = "Gateway" - smile.smile_model_id = "smile_thermo" - smile.smile_name = "Smile Anna" - smile.connect.return_value = Version("4.0.15") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) - - yield smile - - -@pytest.fixture -def mock_smile_anna_3() -> Generator[MagicMock]: - """Create a 3rd Mock Anna environment for testing exceptions.""" - chosen_env = "m_anna_heatpump_idle" - with patch( - "homeassistant.components.plugwise.coordinator.Smile", autospec=True - ) as smile_mock: - smile = smile_mock.return_value - - smile.gateway_id = "015ae9ea3f964e668e490fa39da3870b" - smile.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" smile.smile_version = "4.0.15" - smile.smile_type = "thermostat" - smile.smile_hostname = "smile98765" - smile.smile_model = "Gateway" - smile.smile_model_id = "smile_thermo" - smile.smile_name = "Smile Anna" - smile.connect.return_value = Version("4.0.15") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) yield smile @pytest.fixture -def mock_smile_p1() -> Generator[MagicMock]: - """Create a Mock P1 DSMR environment for testing exceptions.""" - chosen_env = "p1v4_442_single" +def mock_smile_p1(chosen_env: str, gateway_id: str) -> Generator[MagicMock]: + """Create a base Mock P1 type for testing with different datasets and gateway-ids.""" + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value - smile.gateway_id = "a455b61e52394b2db5081ce025a430f3" + smile.async_update.return_value = data + smile.connect.return_value = Version("4.4.2") + smile.gateway_id = gateway_id smile.heater_id = None - smile.smile_version = "4.4.2" - smile.smile_type = "power" + smile.reboot = True smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = "smile" smile.smile_name = "Smile P1" - smile.connect.return_value = Version("4.4.2") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) - - yield smile - - -@pytest.fixture -def mock_smile_p1_2() -> Generator[MagicMock]: - """Create a Mock P1 3-phase DSMR environment for testing exceptions.""" - chosen_env = "p1v4_442_triple" - with patch( - "homeassistant.components.plugwise.coordinator.Smile", autospec=True - ) as smile_mock: - smile = smile_mock.return_value - - smile.gateway_id = "03e65b16e4b247a29ae0d75a78cb492e" - smile.heater_id = None - smile.smile_version = "4.4.2" smile.smile_type = "power" - smile.smile_hostname = "smile98765" - smile.smile_model = "Gateway" - smile.smile_model_id = "smile" - smile.smile_name = "Smile P1" - smile.connect.return_value = Version("4.4.2") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_version = "4.4.2" yield smile @@ -314,24 +257,23 @@ def mock_smile_p1_2() -> Generator[MagicMock]: def mock_smile_legacy_anna() -> Generator[MagicMock]: """Create a Mock legacy Anna environment for testing exceptions.""" chosen_env = "legacy_anna" + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("1.8.22") smile.gateway_id = "0000aaaa0000aaaa0000aaaa0000aa00" smile.heater_id = "04e4cbfe7f4340f090f85ec3b9e6a950" - smile.smile_version = "1.8.22" - smile.smile_type = "thermostat" + smile.reboot = False smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_model_id = None smile.smile_name = "Smile Anna" - smile.connect.return_value = Version("1.8.22") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "thermostat" + smile.smile_version = "1.8.22" yield smile @@ -340,24 +282,23 @@ def mock_smile_legacy_anna() -> Generator[MagicMock]: def mock_stretch() -> Generator[MagicMock]: """Create a Mock Stretch environment for testing exceptions.""" chosen_env = "stretch_v31" + data = _read_json(chosen_env, "data") with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value + smile.async_update.return_value = data + smile.connect.return_value = Version("3.1.11") smile.gateway_id = "259882df3c05415b99c2d962534ce820" smile.heater_id = None - smile.smile_version = "3.1.11" - smile.smile_type = "stretch" + smile.reboot = False smile.smile_hostname = "stretch98765" smile.smile_model = "Gateway" smile.smile_model_id = None smile.smile_name = "Stretch" - smile.connect.return_value = Version("3.1.11") - all_data = _read_json(chosen_env, "all_data") - smile.async_update.return_value = PlugwiseData( - all_data["devices"], all_data["gateway"] - ) + smile.smile_type = "stretch" + smile.smile_version = "3.1.11" yield smile diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/data.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/data.json new file mode 100644 index 00000000000..ab6bdf08e95 --- /dev/null +++ b/tests/components/plugwise/fixtures/anna_heatpump_heating/data.json @@ -0,0 +1,97 @@ +{ + "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 20.2 + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": true, + "cooling_enabled": false, + "cooling_state": false, + "dhw_state": false, + "flame_state": false, + "heating_state": true, + "secondary_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 46.3, + "intended_boiler_temperature": 35.0, + "modulation_level": 52, + "outdoor_air_temperature": 3.0, + "return_temperature": 25.1, + "water_pressure": 1.57, + "water_temperature": 29.1 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard", "off"], + "climate_mode": "auto", + "control_state": "heating", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "select_schedule": "standaard", + "sensors": { + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "temperature": 19.3 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/legacy_anna/data.json b/tests/components/plugwise/fixtures/legacy_anna/data.json new file mode 100644 index 00000000000..cc7e66fb174 --- /dev/null +++ b/tests/components/plugwise/fixtures/legacy_anna/data.json @@ -0,0 +1,60 @@ +{ + "0000aaaa0000aaaa0000aaaa0000aa00": { + "dev_class": "gateway", + "firmware": "1.8.22", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": "01:23:45:67:89:AB", + "model": "Gateway", + "name": "Smile Anna", + "vendor": "Plugwise" + }, + "04e4cbfe7f4340f090f85ec3b9e6a950": { + "binary_sensors": { + "flame_state": true, + "heating_state": true + }, + "dev_class": "heater_central", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "maximum_boiler_temperature": { + "lower_bound": 50.0, + "resolution": 1.0, + "setpoint": 50.0, + "upper_bound": 90.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 51.2, + "intended_boiler_temperature": 17.0, + "modulation_level": 0.0, + "return_temperature": 21.7, + "water_pressure": 1.2, + "water_temperature": 23.6 + }, + "vendor": "Bosch Thermotechniek B.V." + }, + "0d266432d64443e283b5d708ae98b455": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "heating", + "dev_class": "thermostat", + "firmware": "2017-03-13T11:54:58+01:00", + "hardware": "6539-1301-500", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["away", "vacation", "asleep", "home", "no_frost"], + "sensors": { + "illuminance": 150.8, + "setpoint": 20.5, + "temperature": 20.4 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/data.json b/tests/components/plugwise/fixtures/m_adam_cooling/data.json new file mode 100644 index 00000000000..51f19ca3c03 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_cooling/data.json @@ -0,0 +1,203 @@ +{ + "056ee145a816487eaa69243c3280f8bf": { + "available": true, + "binary_sensors": { + "cooling_state": true, + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 50.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 17.5, + "water_temperature": 19.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "1772a4ea304041adb83f357b751341ff": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Badkamer", + "sensors": { + "battery": 99, + "setpoint": 18.0, + "temperature": 21.6, + "temperature_difference": -0.2, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C8FF5EE" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "available": true, + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "ThermoTouch", + "model_id": "143.1", + "name": "Anna", + "sensors": { + "setpoint": 23.5, + "temperature": 25.8 + }, + "vendor": "Plugwise" + }, + "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.7.8", + "gateway_modes": ["away", "full", "vacation"], + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "012345679891", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": [ + "bleeding_hot", + "bleeding_cold", + "off", + "heating", + "cooling" + ], + "select_gateway_mode": "full", + "select_regulation_mode": "cooling", + "sensors": { + "outdoor_temperature": 29.65 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000D5A168D" + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "available": true, + "binary_sensors": { + "low_battery": true + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "model_id": "158-01", + "name": "Lisa Badkamer", + "sensors": { + "battery": 14, + "setpoint": 23.5, + "temperature": 23.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C869B61" + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "f2bf9048bef64cc5b6d5110154e33c81": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "cool", + "control_state": "cooling", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Living room", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "off", + "sensors": { + "electricity_consumed": 149.9, + "electricity_produced": 0.0, + "temperature": 25.8 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint": 23.5, + "upper_bound": 35.0 + }, + "thermostats": { + "primary": ["ad4838d7d35c4d6ea796ee12ae5aedf8"], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "f871b8c4d63549319221e294e4f88074": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "auto", + "control_state": "cooling", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bathroom", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "Badkamer", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 23.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 25.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["e2f4322d57924fa090fbbc48b3a140dc"], + "secondary": ["1772a4ea304041adb83f357b751341ff"] + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_heating/data.json b/tests/components/plugwise/fixtures/m_adam_heating/data.json new file mode 100644 index 00000000000..b10ff8ec2a8 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_heating/data.json @@ -0,0 +1,202 @@ +{ + "056ee145a816487eaa69243c3280f8bf": { + "available": true, + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": true + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 50.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 38.1, + "water_temperature": 37.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "1772a4ea304041adb83f357b751341ff": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Badkamer", + "sensors": { + "battery": 99, + "setpoint": 18.0, + "temperature": 18.6, + "temperature_difference": -0.2, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C8FF5EE" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "available": true, + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "ThermoTouch", + "model_id": "143.1", + "name": "Anna", + "sensors": { + "setpoint": 20.0, + "temperature": 19.1 + }, + "vendor": "Plugwise" + }, + "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.7.8", + "gateway_modes": ["away", "full", "vacation"], + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "012345679891", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": ["bleeding_hot", "bleeding_cold", "off", "heating"], + "select_gateway_mode": "full", + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": -1.25 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000D5A168D" + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "available": true, + "binary_sensors": { + "low_battery": true + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "model_id": "158-01", + "name": "Lisa Badkamer", + "sensors": { + "battery": 14, + "setpoint": 15.0, + "temperature": 17.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C869B61" + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "model": "Switchgroup", + "name": "Test", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "f2bf9048bef64cc5b6d5110154e33c81": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "heat", + "control_state": "preheating", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Living room", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "off", + "sensors": { + "electricity_consumed": 149.9, + "electricity_produced": 0.0, + "temperature": 19.1 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint": 20.0, + "upper_bound": 35.0 + }, + "thermostats": { + "primary": ["ad4838d7d35c4d6ea796ee12ae5aedf8"], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "f871b8c4d63549319221e294e4f88074": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Test", + "Vakantie", + "Weekschema", + "off" + ], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bathroom", + "preset_modes": ["no_frost", "asleep", "vacation", "home", "away"], + "select_schedule": "Badkamer", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 17.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["e2f4322d57924fa090fbbc48b3a140dc"], + "secondary": ["1772a4ea304041adb83f357b751341ff"] + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_jip/data.json b/tests/components/plugwise/fixtures/m_adam_jip/data.json new file mode 100644 index 00000000000..8de57910f66 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_jip/data.json @@ -0,0 +1,370 @@ +{ + "06aecb3d00354375924f50c47af36bd2": { + "active_preset": "no_frost", + "climate_mode": "off", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Slaapkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 24.2 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["1346fbd8498d4dbcab7e18d51b771f3d"], + "secondary": ["356b65335e274d769c338223e7af9c33"] + }, + "vendor": "Plugwise" + }, + "13228dab8ce04617af318a2888b3c548": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Woonkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 27.4 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.01, + "setpoint": 9.0, + "upper_bound": 30.0 + }, + "thermostats": { + "primary": ["f61f1a2535f54f52ad006a3d18e459ca"], + "secondary": ["833de10f269c4deab58fb9df69901b4e"] + }, + "vendor": "Plugwise" + }, + "1346fbd8498d4dbcab7e18d51b771f3d": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "06aecb3d00354375924f50c47af36bd2", + "model": "Lisa", + "model_id": "158-01", + "name": "Slaapkamer", + "sensors": { + "battery": 92, + "setpoint": 13.0, + "temperature": 24.2 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03" + }, + "1da4d325838e4ad8aac12177214505c9": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "d58fec52899f4f1c92e4f8fad6d8c48c", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Logeerkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 28.8, + "temperature_difference": 2.0, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "356b65335e274d769c338223e7af9c33": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "06aecb3d00354375924f50c47af36bd2", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Slaapkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 24.2, + "temperature_difference": 1.7, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05" + }, + "457ce8414de24596a2d5e7dbc9c7682f": { + "available": true, + "dev_class": "zz_misc_plug", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "model": "Aqara Smart Plug", + "model_id": "lumi.plug.maeu01", + "name": "Plug", + "sensors": { + "electricity_consumed_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": false + }, + "vendor": "LUMI", + "zigbee_mac_address": "ABCD012345670A06" + }, + "6f3e9d7084214c21b9dfa46f6eeb8700": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "d27aede973b54be484f6842d1b2802ad", + "model": "Lisa", + "model_id": "158-01", + "name": "Kinderkamer", + "sensors": { + "battery": 79, + "setpoint": 13.0, + "temperature": 30.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "833de10f269c4deab58fb9df69901b4e": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "13228dab8ce04617af318a2888b3c548", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Woonkamer", + "sensors": { + "setpoint": 9.0, + "temperature": 24.0, + "temperature_difference": 1.8, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09" + }, + "a6abc6a129ee499c88a4d420cc413b47": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "d58fec52899f4f1c92e4f8fad6d8c48c", + "model": "Lisa", + "model_id": "158-01", + "name": "Logeerkamer", + "sensors": { + "battery": 80, + "setpoint": 13.0, + "temperature": 30.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "b5c2386c6f6342669e50fe49dd05b188": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.2.8", + "gateway_modes": ["away", "full", "vacation"], + "hardware": "AME Smile 2.0 board", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": ["heating", "off", "bleeding_cold", "bleeding_hot"], + "select_gateway_mode": "full", + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": 24.9 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + }, + "d27aede973b54be484f6842d1b2802ad": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Kinderkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 30.0 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["6f3e9d7084214c21b9dfa46f6eeb8700"], + "secondary": ["d4496250d0e942cfa7aea3476e9070d5"] + }, + "vendor": "Plugwise" + }, + "d4496250d0e942cfa7aea3476e9070d5": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "d27aede973b54be484f6842d1b2802ad", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Tom Kinderkamer", + "sensors": { + "setpoint": 13.0, + "temperature": 28.7, + "temperature_difference": 1.9, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" + }, + "d58fec52899f4f1c92e4f8fad6d8c48c": { + "active_preset": "home", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Logeerkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 30.0 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": ["a6abc6a129ee499c88a4d420cc413b47"], + "secondary": ["1da4d325838e4ad8aac12177214505c9"] + }, + "vendor": "Plugwise" + }, + "e4684553153b44afbef2200885f379dc": { + "available": true, + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "9e4433a9d69f40b3aefd15e74395eaec", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 20.0, + "resolution": 0.01, + "setpoint": 90.0, + "upper_bound": 90.0 + }, + "model": "Generic heater", + "model_id": "10.20", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 0.0, + "modulation_level": 0.0, + "return_temperature": 37.1, + "water_pressure": 1.4, + "water_temperature": 37.3 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Remeha B.V." + }, + "f61f1a2535f54f52ad006a3d18e459ca": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermometer", + "firmware": "2020-09-01T02:00:00+02:00", + "hardware": "1", + "location": "13228dab8ce04617af318a2888b3c548", + "model": "Jip", + "model_id": "168-01", + "name": "Woonkamer", + "sensors": { + "battery": 100, + "humidity": 56.2, + "setpoint": 9.0, + "temperature": 27.4 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08" + } +} diff --git a/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json new file mode 100644 index 00000000000..7c38b1b2197 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_multiple_devices_per_zone/data.json @@ -0,0 +1,584 @@ +{ + "02cf28bfec924855854c544690a609ef": { + "available": true, + "dev_class": "vcr_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "NVR", + "sensors": { + "electricity_consumed": 34.0, + "electricity_consumed_interval": 9.15, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A15" + }, + "08963fec7c53423ca5680aa4cb502c63": { + "active_preset": "away", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Badkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "Badkamer Schema", + "sensors": { + "temperature": 18.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 14.0, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": [ + "f1fee6043d3642a9b0a65297455f008e", + "680423ff840043738f42cc7f1ff97a36" + ], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "12493538af164a409c6a1c79e38afe1c": { + "active_preset": "away", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bios", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "off", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 16.5 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 13.0, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["df4a4a8169904cdb9c03d61a21f42140"], + "secondary": ["a2c3583e0a6349358998b760cea82d2a"] + }, + "vendor": "Plugwise" + }, + "21f2b542c49845e6bb416884c55778d6": { + "available": true, + "dev_class": "game_console_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "Playstation Smart Plug", + "sensors": { + "electricity_consumed": 84.1, + "electricity_consumed_interval": 8.6, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A12" + }, + "446ac08dd04d4eff8ac57489757b7314": { + "active_preset": "no_frost", + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Garage", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "sensors": { + "temperature": 15.6 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 5.5, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["e7693eb9582644e5b865dba8d4447cf1"], + "secondary": [] + }, + "vendor": "Plugwise" + }, + "4a810418d5394b3f82727340b91ba740": { + "available": true, + "dev_class": "router_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "USG Smart Plug", + "sensors": { + "electricity_consumed": 8.5, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A16" + }, + "675416a629f343c495449970e2ca37b5": { + "available": true, + "dev_class": "router_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "Ziggo Modem", + "sensors": { + "electricity_consumed": 12.2, + "electricity_consumed_interval": 2.97, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "680423ff840043738f42cc7f1ff97a36": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "08963fec7c53423ca5680aa4cb502c63", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Thermostatic Radiator Badkamer 1", + "sensors": { + "battery": 51, + "setpoint": 14.0, + "temperature": 19.1, + "temperature_difference": -0.4, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A17" + }, + "6a3bf693d05e48e0b460c815a4fdd09d": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "82fa13f017d240daa0d0ea1775420f24", + "model": "Lisa", + "model_id": "158-01", + "name": "Zone Thermostat Jessie", + "sensors": { + "battery": 37, + "setpoint": 15.0, + "temperature": 17.2 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A03" + }, + "78d1126fc4c743db81b61c20e88342a7": { + "available": true, + "dev_class": "central_heating_pump_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Plug", + "model_id": "160-01", + "name": "CV Pomp", + "sensors": { + "electricity_consumed": 35.6, + "electricity_consumed_interval": 7.37, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A05" + }, + "82fa13f017d240daa0d0ea1775420f24": { + "active_preset": "asleep", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Jessie", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "CV Jessie", + "sensors": { + "temperature": 17.2 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["6a3bf693d05e48e0b460c815a4fdd09d"], + "secondary": ["d3da73bde12a47d5a6b8f9dad971f2ec"] + }, + "vendor": "Plugwise" + }, + "90986d591dcd426cae3ec3e8111ff730": { + "binary_sensors": { + "heating_state": true + }, + "dev_class": "heater_central", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "model": "Unknown", + "name": "OnOff", + "sensors": { + "intended_boiler_temperature": 70.0, + "modulation_level": 1, + "water_temperature": 70.0 + } + }, + "a28f588dc4a049a483fd03a30361ad3a": { + "available": true, + "dev_class": "settop_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "Fibaro HC2", + "sensors": { + "electricity_consumed": 12.5, + "electricity_consumed_interval": 3.8, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A13" + }, + "a2c3583e0a6349358998b760cea82d2a": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "12493538af164a409c6a1c79e38afe1c", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Bios Cv Thermostatic Radiator ", + "sensors": { + "battery": 62, + "setpoint": 13.0, + "temperature": 17.2, + "temperature_difference": -0.2, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A09" + }, + "b310b72a0e354bfab43089919b9a88bf": { + "available": true, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Floor kraan", + "sensors": { + "setpoint": 21.5, + "temperature": 26.0, + "temperature_difference": 3.5, + "valve_position": 100 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "b59bcebaf94b499ea7d46e4a66fb62d8": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-08-02T02:00:00+02:00", + "hardware": "255", + "location": "c50f167537524366a5af7aa3942feb1e", + "model": "Lisa", + "model_id": "158-01", + "name": "Zone Lisa WK", + "sensors": { + "battery": 34, + "setpoint": 21.5, + "temperature": 20.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "c50f167537524366a5af7aa3942feb1e": { + "active_preset": "home", + "available_schedules": [ + "CV Roan", + "Bios Schema met Film Avond", + "GF7 Woonkamer", + "Badkamer Schema", + "CV Jessie", + "off" + ], + "climate_mode": "auto", + "control_state": "heating", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Woonkamer", + "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "select_schedule": "GF7 Woonkamer", + "sensors": { + "electricity_consumed": 35.6, + "electricity_produced": 0.0, + "temperature": 20.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 21.5, + "upper_bound": 100.0 + }, + "thermostats": { + "primary": ["b59bcebaf94b499ea7d46e4a66fb62d8"], + "secondary": ["b310b72a0e354bfab43089919b9a88bf"] + }, + "vendor": "Plugwise" + }, + "cd0ddb54ef694e11ac18ed1cbce5dbbd": { + "available": true, + "dev_class": "vcr_plug", + "firmware": "2019-06-21T02:00:00+02:00", + "location": "cd143c07248f491493cea0533bc3d669", + "model": "Plug", + "model_id": "160-01", + "name": "NAS", + "sensors": { + "electricity_consumed": 16.5, + "electricity_consumed_interval": 0.5, + "electricity_produced": 0.0, + "electricity_produced_interval": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A14" + }, + "d3da73bde12a47d5a6b8f9dad971f2ec": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "82fa13f017d240daa0d0ea1775420f24", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "Thermostatic Radiator Jessie", + "sensors": { + "battery": 62, + "setpoint": 15.0, + "temperature": 17.1, + "temperature_difference": 0.1, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A10" + }, + "df4a4a8169904cdb9c03d61a21f42140": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "12493538af164a409c6a1c79e38afe1c", + "model": "Lisa", + "model_id": "158-01", + "name": "Zone Lisa Bios", + "sensors": { + "battery": 67, + "setpoint": 13.0, + "temperature": 16.5 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A06" + }, + "e7693eb9582644e5b865dba8d4447cf1": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", + "location": "446ac08dd04d4eff8ac57489757b7314", + "model": "Tom/Floor", + "model_id": "106-03", + "name": "CV Kraan Garage", + "sensors": { + "battery": 68, + "setpoint": 5.5, + "temperature": 15.6, + "temperature_difference": 0.0, + "valve_position": 0.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A11" + }, + "f1fee6043d3642a9b0a65297455f008e": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", + "location": "08963fec7c53423ca5680aa4cb502c63", + "model": "Lisa", + "model_id": "158-01", + "name": "Thermostatic Radiator Badkamer 2", + "sensors": { + "battery": 92, + "setpoint": 14.0, + "temperature": 18.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A08" + }, + "fe799307f1624099878210aa0b9f1475": { + "binary_sensors": { + "plugwise_notification": true + }, + "dev_class": "gateway", + "firmware": "3.0.15", + "hardware": "AME Smile 2.0 board", + "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": { + "af82e4ccf9c548528166d38e560662a4": { + "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." + } + }, + "select_regulation_mode": "heating", + "sensors": { + "outdoor_temperature": 7.81 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + } +} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/data.json new file mode 100644 index 00000000000..ccfd816ff63 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/data.json @@ -0,0 +1,97 @@ +{ + "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 28.2 + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": true, + "cooling_enabled": true, + "cooling_state": true, + "dhw_state": false, + "flame_state": false, + "heating_state": false, + "secondary_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 41.5, + "intended_boiler_temperature": 0.0, + "modulation_level": 40, + "outdoor_air_temperature": 28.0, + "return_temperature": 23.8, + "water_pressure": 1.57, + "water_temperature": 22.7 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard", "off"], + "climate_mode": "auto", + "control_state": "cooling", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "select_schedule": "standaard", + "sensors": { + "cooling_activation_outdoor_temperature": 21.0, + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "temperature": 26.3 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/data.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/data.json new file mode 100644 index 00000000000..5a1cdebd380 --- /dev/null +++ b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/data.json @@ -0,0 +1,97 @@ +{ + "015ae9ea3f964e668e490fa39da3870b": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 28.2 + }, + "vendor": "Plugwise" + }, + "1cbf783bb11e4a7c8a6843dee3a86927": { + "available": true, + "binary_sensors": { + "compressor_state": false, + "cooling_enabled": true, + "cooling_state": false, + "dhw_state": false, + "flame_state": false, + "heating_state": false, + "secondary_boiler_state": false + }, + "dev_class": "heater_central", + "location": "a57efe5f145f498c9be62a9b63626fbf", + "max_dhw_temperature": { + "lower_bound": 35.0, + "resolution": 0.01, + "setpoint": 53.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 60.0, + "upper_bound": 100.0 + }, + "model": "Generic heater/cooler", + "name": "OpenTherm", + "sensors": { + "dhw_temperature": 46.3, + "intended_boiler_temperature": 18.0, + "modulation_level": 0, + "outdoor_air_temperature": 28.2, + "return_temperature": 22.0, + "water_pressure": 1.57, + "water_temperature": 19.1 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Techneco" + }, + "3cb70739631c4d17a86b8b12e8a5161b": { + "active_preset": "home", + "available_schedules": ["standaard", "off"], + "climate_mode": "auto", + "control_state": "idle", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "c784ee9fdab44e1395b8dee7d7a497d5", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], + "select_schedule": "standaard", + "sensors": { + "cooling_activation_outdoor_temperature": 25.0, + "cooling_deactivation_threshold": 4.0, + "illuminance": 86.0, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "temperature": 23.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": -0.5, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint_high": 30.0, + "setpoint_low": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/p1v4_442_single/data.json b/tests/components/plugwise/fixtures/p1v4_442_single/data.json new file mode 100644 index 00000000000..6dfcd7ee033 --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_single/data.json @@ -0,0 +1,43 @@ +{ + "a455b61e52394b2db5081ce025a430f3": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "a455b61e52394b2db5081ce025a430f3", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile", + "name": "Smile P1", + "notifications": {}, + "vendor": "Plugwise" + }, + "ba4de7613517478da82dd9b6abea36af": { + "available": true, + "dev_class": "smartmeter", + "location": "a455b61e52394b2db5081ce025a430f3", + "model": "KFM5KAIFA-METER", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 17643.423, + "electricity_consumed_off_peak_interval": 15, + "electricity_consumed_off_peak_point": 486, + "electricity_consumed_peak_cumulative": 13966.608, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_phase_one_consumed": 486, + "electricity_phase_one_produced": 0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_peak_point": 0, + "net_electricity_cumulative": 31610.031, + "net_electricity_point": 486 + }, + "vendor": "SHENZHEN KAIFA TECHNOLOGY \uff08CHENGDU\uff09 CO., LTD." + } +} diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/data.json b/tests/components/plugwise/fixtures/p1v4_442_triple/data.json new file mode 100644 index 00000000000..943325d1415 --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_triple/data.json @@ -0,0 +1,56 @@ +{ + "03e65b16e4b247a29ae0d75a78cb492e": { + "binary_sensors": { + "plugwise_notification": true + }, + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile", + "name": "Smile P1", + "notifications": { + "97a04c0c263049b29350a660b4cdd01e": { + "warning": "The Smile P1 is not connected to a smart meter." + } + }, + "vendor": "Plugwise" + }, + "b82b6b3322484f2ea4e25e0bd5f3d61f": { + "available": true, + "dev_class": "smartmeter", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "model": "XMX5LGF0010453051839", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 70537.898, + "electricity_consumed_off_peak_interval": 314, + "electricity_consumed_off_peak_point": 5553, + "electricity_consumed_peak_cumulative": 161328.641, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_phase_one_consumed": 1763, + "electricity_phase_one_produced": 0, + "electricity_phase_three_consumed": 2080, + "electricity_phase_three_produced": 0, + "electricity_phase_two_consumed": 1703, + "electricity_phase_two_produced": 0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_peak_point": 0, + "gas_consumed_cumulative": 16811.37, + "gas_consumed_interval": 0.06, + "net_electricity_cumulative": 231866.539, + "net_electricity_point": 5553, + "voltage_phase_one": 233.2, + "voltage_phase_three": 234.7, + "voltage_phase_two": 234.4 + }, + "vendor": "XEMEX NV" + } +} diff --git a/tests/components/plugwise/fixtures/smile_p1_v2/data.json b/tests/components/plugwise/fixtures/smile_p1_v2/data.json new file mode 100644 index 00000000000..768dd2c2334 --- /dev/null +++ b/tests/components/plugwise/fixtures/smile_p1_v2/data.json @@ -0,0 +1,34 @@ +{ + "938696c4bcdb4b8a9a595cb38ed43913": { + "dev_class": "smartmeter", + "location": "938696c4bcdb4b8a9a595cb38ed43913", + "model": "Ene5\\T210-DESMR5.0", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 1642.74, + "electricity_consumed_off_peak_interval": 0, + "electricity_consumed_peak_cumulative": 1155.195, + "electricity_consumed_peak_interval": 250, + "electricity_consumed_point": 458, + "electricity_produced_off_peak_cumulative": 482.598, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_peak_cumulative": 1296.136, + "electricity_produced_peak_interval": 0, + "electricity_produced_point": 0, + "gas_consumed_cumulative": 584.433, + "gas_consumed_interval": 0.016, + "net_electricity_cumulative": 1019.201, + "net_electricity_point": 458 + }, + "vendor": "Ene5\\T210-DESMR5.0" + }, + "aaaa0000aaaa0000aaaa0000aaaa00aa": { + "dev_class": "gateway", + "firmware": "2.5.9", + "location": "938696c4bcdb4b8a9a595cb38ed43913", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Smile P1", + "vendor": "Plugwise" + } +} diff --git a/tests/components/plugwise/fixtures/stretch_v31/data.json b/tests/components/plugwise/fixtures/stretch_v31/data.json new file mode 100644 index 00000000000..250839d08a8 --- /dev/null +++ b/tests/components/plugwise/fixtures/stretch_v31/data.json @@ -0,0 +1,136 @@ +{ + "0000aaaa0000aaaa0000aaaa0000aa00": { + "dev_class": "gateway", + "firmware": "3.1.11", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "mac_address": "01:23:45:67:89:AB", + "model": "Gateway", + "name": "Stretch", + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670101" + }, + "059e4d03c7a34d278add5c7a4a781d19": { + "dev_class": "washingmachine", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Wasmachine (52AC1)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": true, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A01" + }, + "5871317346d045bc9f6b987ef25ee638": { + "dev_class": "water_heater_vessel", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "6539-0701-4028", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Boiler (1EB31)", + "sensors": { + "electricity_consumed": 1.19, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A07" + }, + "aac7b735042c4832ac9ff33aae4f453b": { + "dev_class": "dishwasher", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "6539-0701-4022", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Vaatwasser (2a1ab)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.71, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A02" + }, + "cfe95cf3de1948c0b8955125bf754614": { + "dev_class": "dryer", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle type F", + "name": "Droger (52559)", + "sensors": { + "electricity_consumed": 0.0, + "electricity_consumed_interval": 0.0, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "ABCD012345670A04" + }, + "d03738edfcc947f7b8f4573571d90d2d": { + "dev_class": "switching", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "cfe95cf3de1948c0b8955125bf754614" + ], + "model": "Switchgroup", + "name": "Schakel", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "d950b314e9d8499f968e6db8d82ef78c": { + "dev_class": "report", + "members": [ + "059e4d03c7a34d278add5c7a4a781d19", + "5871317346d045bc9f6b987ef25ee638", + "aac7b735042c4832ac9ff33aae4f453b", + "cfe95cf3de1948c0b8955125bf754614", + "e1c884e7dede431dadee09506ec4f859" + ], + "model": "Switchgroup", + "name": "Stroomvreters", + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "e1c884e7dede431dadee09506ec4f859": { + "dev_class": "refrigerator", + "firmware": "2011-06-27T10:47:37+02:00", + "hardware": "6539-0700-7330", + "location": "0000aaaa0000aaaa0000aaaa0000aa00", + "model": "Circle+ type F", + "name": "Koelkast (92C4A)", + "sensors": { + "electricity_consumed": 50.5, + "electricity_consumed_interval": 0.08, + "electricity_produced": 0.0 + }, + "switches": { + "lock": false, + "relay": true + }, + "vendor": "Plugwise", + "zigbee_mac_address": "0123456789AB" + } +} diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index 806c92fe7cb..92ed327b841 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -1,643 +1,633 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'devices': dict({ - '02cf28bfec924855854c544690a609ef': dict({ - 'available': True, - 'dev_class': 'vcr_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'NVR', - 'sensors': dict({ - 'electricity_consumed': 34.0, - 'electricity_consumed_interval': 9.15, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A15', + '02cf28bfec924855854c544690a609ef': dict({ + 'available': True, + 'dev_class': 'vcr_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'NVR', + 'sensors': dict({ + 'electricity_consumed': 34.0, + 'electricity_consumed_interval': 9.15, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, }), - '08963fec7c53423ca5680aa4cb502c63': dict({ - 'active_preset': 'away', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A15', + }), + '08963fec7c53423ca5680aa4cb502c63': dict({ + 'active_preset': 'away', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'auto', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Badkamer', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'Badkamer Schema', + 'sensors': dict({ + 'temperature': 18.9, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 14.0, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'f1fee6043d3642a9b0a65297455f008e', + '680423ff840043738f42cc7f1ff97a36', ]), - 'climate_mode': 'auto', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Badkamer', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + 'secondary': list([ ]), - 'select_schedule': 'Badkamer Schema', - 'sensors': dict({ - 'temperature': 18.9, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 14.0, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'f1fee6043d3642a9b0a65297455f008e', - '680423ff840043738f42cc7f1ff97a36', - ]), - 'secondary': list([ - ]), - }), - 'vendor': 'Plugwise', }), - '12493538af164a409c6a1c79e38afe1c': dict({ - 'active_preset': 'away', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'vendor': 'Plugwise', + }), + '12493538af164a409c6a1c79e38afe1c': dict({ + 'active_preset': 'away', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'heat', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Bios', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'off', + 'sensors': dict({ + 'electricity_consumed': 0.0, + 'electricity_produced': 0.0, + 'temperature': 16.5, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 13.0, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'df4a4a8169904cdb9c03d61a21f42140', ]), - 'climate_mode': 'heat', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Bios', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + 'secondary': list([ + 'a2c3583e0a6349358998b760cea82d2a', ]), - 'select_schedule': 'off', - 'sensors': dict({ - 'electricity_consumed': 0.0, - 'electricity_produced': 0.0, - 'temperature': 16.5, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 13.0, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'df4a4a8169904cdb9c03d61a21f42140', - ]), - 'secondary': list([ - 'a2c3583e0a6349358998b760cea82d2a', - ]), - }), - 'vendor': 'Plugwise', }), - '21f2b542c49845e6bb416884c55778d6': dict({ - 'available': True, - 'dev_class': 'game_console_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'Playstation Smart Plug', - 'sensors': dict({ - 'electricity_consumed': 84.1, - 'electricity_consumed_interval': 8.6, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': False, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A12', + 'vendor': 'Plugwise', + }), + '21f2b542c49845e6bb416884c55778d6': dict({ + 'available': True, + 'dev_class': 'game_console_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'Playstation Smart Plug', + 'sensors': dict({ + 'electricity_consumed': 84.1, + 'electricity_consumed_interval': 8.6, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, }), - '446ac08dd04d4eff8ac57489757b7314': dict({ - 'active_preset': 'no_frost', - 'climate_mode': 'heat', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Garage', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + 'switches': dict({ + 'lock': False, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A12', + }), + '446ac08dd04d4eff8ac57489757b7314': dict({ + 'active_preset': 'no_frost', + 'climate_mode': 'heat', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Garage', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'sensors': dict({ + 'temperature': 15.6, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 5.5, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'e7693eb9582644e5b865dba8d4447cf1', ]), - 'sensors': dict({ - 'temperature': 15.6, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 5.5, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'e7693eb9582644e5b865dba8d4447cf1', - ]), - 'secondary': list([ - ]), - }), - 'vendor': 'Plugwise', - }), - '4a810418d5394b3f82727340b91ba740': dict({ - 'available': True, - 'dev_class': 'router_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'USG Smart Plug', - 'sensors': dict({ - 'electricity_consumed': 8.5, - 'electricity_consumed_interval': 0.0, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A16', - }), - '675416a629f343c495449970e2ca37b5': dict({ - 'available': True, - 'dev_class': 'router_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'Ziggo Modem', - 'sensors': dict({ - 'electricity_consumed': 12.2, - 'electricity_consumed_interval': 2.97, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A01', - }), - '680423ff840043738f42cc7f1ff97a36': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '08963fec7c53423ca5680aa4cb502c63', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Thermostatic Radiator Badkamer 1', - 'sensors': dict({ - 'battery': 51, - 'setpoint': 14.0, - 'temperature': 19.1, - 'temperature_difference': -0.4, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A17', - }), - '6a3bf693d05e48e0b460c815a4fdd09d': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'zone_thermostat', - 'firmware': '2016-10-27T02:00:00+02:00', - 'hardware': '255', - 'location': '82fa13f017d240daa0d0ea1775420f24', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Zone Thermostat Jessie', - 'sensors': dict({ - 'battery': 37, - 'setpoint': 15.0, - 'temperature': 17.2, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A03', - }), - '78d1126fc4c743db81b61c20e88342a7': dict({ - 'available': True, - 'dev_class': 'central_heating_pump_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'c50f167537524366a5af7aa3942feb1e', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'CV Pomp', - 'sensors': dict({ - 'electricity_consumed': 35.6, - 'electricity_consumed_interval': 7.37, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A05', - }), - '82fa13f017d240daa0d0ea1775420f24': dict({ - 'active_preset': 'asleep', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'secondary': list([ ]), - 'climate_mode': 'auto', - 'control_state': 'idle', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Jessie', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', + }), + 'vendor': 'Plugwise', + }), + '4a810418d5394b3f82727340b91ba740': dict({ + 'available': True, + 'dev_class': 'router_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'USG Smart Plug', + 'sensors': dict({ + 'electricity_consumed': 8.5, + 'electricity_consumed_interval': 0.0, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A16', + }), + '675416a629f343c495449970e2ca37b5': dict({ + 'available': True, + 'dev_class': 'router_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'Ziggo Modem', + 'sensors': dict({ + 'electricity_consumed': 12.2, + 'electricity_consumed_interval': 2.97, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A01', + }), + '680423ff840043738f42cc7f1ff97a36': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '08963fec7c53423ca5680aa4cb502c63', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Thermostatic Radiator Badkamer 1', + 'sensors': dict({ + 'battery': 51, + 'setpoint': 14.0, + 'temperature': 19.1, + 'temperature_difference': -0.4, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A17', + }), + '6a3bf693d05e48e0b460c815a4fdd09d': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'zone_thermostat', + 'firmware': '2016-10-27T02:00:00+02:00', + 'hardware': '255', + 'location': '82fa13f017d240daa0d0ea1775420f24', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Zone Thermostat Jessie', + 'sensors': dict({ + 'battery': 37, + 'setpoint': 15.0, + 'temperature': 17.2, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A03', + }), + '78d1126fc4c743db81b61c20e88342a7': dict({ + 'available': True, + 'dev_class': 'central_heating_pump_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'c50f167537524366a5af7aa3942feb1e', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'CV Pomp', + 'sensors': dict({ + 'electricity_consumed': 35.6, + 'electricity_consumed_interval': 7.37, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A05', + }), + '82fa13f017d240daa0d0ea1775420f24': dict({ + 'active_preset': 'asleep', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'auto', + 'control_state': 'idle', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Jessie', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'CV Jessie', + 'sensors': dict({ + 'temperature': 17.2, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 15.0, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + '6a3bf693d05e48e0b460c815a4fdd09d', ]), - 'select_schedule': 'CV Jessie', - 'sensors': dict({ - 'temperature': 17.2, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 15.0, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - '6a3bf693d05e48e0b460c815a4fdd09d', - ]), - 'secondary': list([ - 'd3da73bde12a47d5a6b8f9dad971f2ec', - ]), - }), - 'vendor': 'Plugwise', - }), - '90986d591dcd426cae3ec3e8111ff730': dict({ - 'binary_sensors': dict({ - 'heating_state': True, - }), - 'dev_class': 'heater_central', - 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', - 'model': 'Unknown', - 'name': 'OnOff', - 'sensors': dict({ - 'intended_boiler_temperature': 70.0, - 'modulation_level': 1, - 'water_temperature': 70.0, - }), - }), - 'a28f588dc4a049a483fd03a30361ad3a': dict({ - 'available': True, - 'dev_class': 'settop_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'Fibaro HC2', - 'sensors': dict({ - 'electricity_consumed': 12.5, - 'electricity_consumed_interval': 3.8, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A13', - }), - 'a2c3583e0a6349358998b760cea82d2a': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '12493538af164a409c6a1c79e38afe1c', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Bios Cv Thermostatic Radiator ', - 'sensors': dict({ - 'battery': 62, - 'setpoint': 13.0, - 'temperature': 17.2, - 'temperature_difference': -0.2, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A09', - }), - 'b310b72a0e354bfab43089919b9a88bf': dict({ - 'available': True, - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': 'c50f167537524366a5af7aa3942feb1e', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Floor kraan', - 'sensors': dict({ - 'setpoint': 21.5, - 'temperature': 26.0, - 'temperature_difference': 3.5, - 'valve_position': 100, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A02', - }), - 'b59bcebaf94b499ea7d46e4a66fb62d8': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'zone_thermostat', - 'firmware': '2016-08-02T02:00:00+02:00', - 'hardware': '255', - 'location': 'c50f167537524366a5af7aa3942feb1e', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Zone Lisa WK', - 'sensors': dict({ - 'battery': 34, - 'setpoint': 21.5, - 'temperature': 20.9, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A07', - }), - 'c50f167537524366a5af7aa3942feb1e': dict({ - 'active_preset': 'home', - 'available_schedules': list([ - 'CV Roan', - 'Bios Schema met Film Avond', - 'GF7 Woonkamer', - 'Badkamer Schema', - 'CV Jessie', - 'off', + 'secondary': list([ + 'd3da73bde12a47d5a6b8f9dad971f2ec', ]), - 'climate_mode': 'auto', - 'control_state': 'heating', - 'dev_class': 'climate', - 'model': 'ThermoZone', - 'name': 'Woonkamer', - 'preset_modes': list([ - 'home', - 'asleep', - 'away', - 'vacation', - 'no_frost', - ]), - 'select_schedule': 'GF7 Woonkamer', - 'sensors': dict({ - 'electricity_consumed': 35.6, - 'electricity_produced': 0.0, - 'temperature': 20.9, - }), - 'thermostat': dict({ - 'lower_bound': 0.0, - 'resolution': 0.01, - 'setpoint': 21.5, - 'upper_bound': 100.0, - }), - 'thermostats': dict({ - 'primary': list([ - 'b59bcebaf94b499ea7d46e4a66fb62d8', - ]), - 'secondary': list([ - 'b310b72a0e354bfab43089919b9a88bf', - ]), - }), - 'vendor': 'Plugwise', }), - 'cd0ddb54ef694e11ac18ed1cbce5dbbd': dict({ - 'available': True, - 'dev_class': 'vcr_plug', - 'firmware': '2019-06-21T02:00:00+02:00', - 'location': 'cd143c07248f491493cea0533bc3d669', - 'model': 'Plug', - 'model_id': '160-01', - 'name': 'NAS', - 'sensors': dict({ - 'electricity_consumed': 16.5, - 'electricity_consumed_interval': 0.5, - 'electricity_produced': 0.0, - 'electricity_produced_interval': 0.0, - }), - 'switches': dict({ - 'lock': True, - 'relay': True, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A14', + 'vendor': 'Plugwise', + }), + '90986d591dcd426cae3ec3e8111ff730': dict({ + 'binary_sensors': dict({ + 'heating_state': True, }), - 'd3da73bde12a47d5a6b8f9dad971f2ec': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '82fa13f017d240daa0d0ea1775420f24', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'Thermostatic Radiator Jessie', - 'sensors': dict({ - 'battery': 62, - 'setpoint': 15.0, - 'temperature': 17.1, - 'temperature_difference': 0.1, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A10', - }), - 'df4a4a8169904cdb9c03d61a21f42140': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'zone_thermostat', - 'firmware': '2016-10-27T02:00:00+02:00', - 'hardware': '255', - 'location': '12493538af164a409c6a1c79e38afe1c', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Zone Lisa Bios', - 'sensors': dict({ - 'battery': 67, - 'setpoint': 13.0, - 'temperature': 16.5, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A06', - }), - 'e7693eb9582644e5b865dba8d4447cf1': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2019-03-27T01:00:00+01:00', - 'hardware': '1', - 'location': '446ac08dd04d4eff8ac57489757b7314', - 'model': 'Tom/Floor', - 'model_id': '106-03', - 'name': 'CV Kraan Garage', - 'sensors': dict({ - 'battery': 68, - 'setpoint': 5.5, - 'temperature': 15.6, - 'temperature_difference': 0.0, - 'valve_position': 0.0, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A11', - }), - 'f1fee6043d3642a9b0a65297455f008e': dict({ - 'available': True, - 'binary_sensors': dict({ - 'low_battery': False, - }), - 'dev_class': 'thermostatic_radiator_valve', - 'firmware': '2016-10-27T02:00:00+02:00', - 'hardware': '255', - 'location': '08963fec7c53423ca5680aa4cb502c63', - 'model': 'Lisa', - 'model_id': '158-01', - 'name': 'Thermostatic Radiator Badkamer 2', - 'sensors': dict({ - 'battery': 92, - 'setpoint': 14.0, - 'temperature': 18.9, - }), - 'temperature_offset': dict({ - 'lower_bound': -2.0, - 'resolution': 0.1, - 'setpoint': 0.0, - 'upper_bound': 2.0, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670A08', - }), - 'fe799307f1624099878210aa0b9f1475': dict({ - 'binary_sensors': dict({ - 'plugwise_notification': True, - }), - 'dev_class': 'gateway', - 'firmware': '3.0.15', - 'hardware': 'AME Smile 2.0 board', - 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', - 'mac_address': '012345670001', - 'model': 'Gateway', - 'model_id': 'smile_open_therm', - 'name': 'Adam', - 'select_regulation_mode': 'heating', - 'sensors': dict({ - 'outdoor_temperature': 7.81, - }), - 'vendor': 'Plugwise', - 'zigbee_mac_address': 'ABCD012345670101', + 'dev_class': 'heater_central', + 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', + 'model': 'Unknown', + 'name': 'OnOff', + 'sensors': dict({ + 'intended_boiler_temperature': 70.0, + 'modulation_level': 1, + 'water_temperature': 70.0, }), }), - 'gateway': dict({ - 'cooling_present': False, - 'gateway_id': 'fe799307f1624099878210aa0b9f1475', - 'heater_id': '90986d591dcd426cae3ec3e8111ff730', - 'item_count': 369, + 'a28f588dc4a049a483fd03a30361ad3a': dict({ + 'available': True, + 'dev_class': 'settop_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'Fibaro HC2', + 'sensors': dict({ + 'electricity_consumed': 12.5, + 'electricity_consumed_interval': 3.8, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A13', + }), + 'a2c3583e0a6349358998b760cea82d2a': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '12493538af164a409c6a1c79e38afe1c', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Bios Cv Thermostatic Radiator ', + 'sensors': dict({ + 'battery': 62, + 'setpoint': 13.0, + 'temperature': 17.2, + 'temperature_difference': -0.2, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A09', + }), + 'b310b72a0e354bfab43089919b9a88bf': dict({ + 'available': True, + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': 'c50f167537524366a5af7aa3942feb1e', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Floor kraan', + 'sensors': dict({ + 'setpoint': 21.5, + 'temperature': 26.0, + 'temperature_difference': 3.5, + 'valve_position': 100, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A02', + }), + 'b59bcebaf94b499ea7d46e4a66fb62d8': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'zone_thermostat', + 'firmware': '2016-08-02T02:00:00+02:00', + 'hardware': '255', + 'location': 'c50f167537524366a5af7aa3942feb1e', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Zone Lisa WK', + 'sensors': dict({ + 'battery': 34, + 'setpoint': 21.5, + 'temperature': 20.9, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A07', + }), + 'c50f167537524366a5af7aa3942feb1e': dict({ + 'active_preset': 'home', + 'available_schedules': list([ + 'CV Roan', + 'Bios Schema met Film Avond', + 'GF7 Woonkamer', + 'Badkamer Schema', + 'CV Jessie', + 'off', + ]), + 'climate_mode': 'auto', + 'control_state': 'heating', + 'dev_class': 'climate', + 'model': 'ThermoZone', + 'name': 'Woonkamer', + 'preset_modes': list([ + 'home', + 'asleep', + 'away', + 'vacation', + 'no_frost', + ]), + 'select_schedule': 'GF7 Woonkamer', + 'sensors': dict({ + 'electricity_consumed': 35.6, + 'electricity_produced': 0.0, + 'temperature': 20.9, + }), + 'thermostat': dict({ + 'lower_bound': 0.0, + 'resolution': 0.01, + 'setpoint': 21.5, + 'upper_bound': 100.0, + }), + 'thermostats': dict({ + 'primary': list([ + 'b59bcebaf94b499ea7d46e4a66fb62d8', + ]), + 'secondary': list([ + 'b310b72a0e354bfab43089919b9a88bf', + ]), + }), + 'vendor': 'Plugwise', + }), + 'cd0ddb54ef694e11ac18ed1cbce5dbbd': dict({ + 'available': True, + 'dev_class': 'vcr_plug', + 'firmware': '2019-06-21T02:00:00+02:00', + 'location': 'cd143c07248f491493cea0533bc3d669', + 'model': 'Plug', + 'model_id': '160-01', + 'name': 'NAS', + 'sensors': dict({ + 'electricity_consumed': 16.5, + 'electricity_consumed_interval': 0.5, + 'electricity_produced': 0.0, + 'electricity_produced_interval': 0.0, + }), + 'switches': dict({ + 'lock': True, + 'relay': True, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A14', + }), + 'd3da73bde12a47d5a6b8f9dad971f2ec': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '82fa13f017d240daa0d0ea1775420f24', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'Thermostatic Radiator Jessie', + 'sensors': dict({ + 'battery': 62, + 'setpoint': 15.0, + 'temperature': 17.1, + 'temperature_difference': 0.1, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A10', + }), + 'df4a4a8169904cdb9c03d61a21f42140': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'zone_thermostat', + 'firmware': '2016-10-27T02:00:00+02:00', + 'hardware': '255', + 'location': '12493538af164a409c6a1c79e38afe1c', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Zone Lisa Bios', + 'sensors': dict({ + 'battery': 67, + 'setpoint': 13.0, + 'temperature': 16.5, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A06', + }), + 'e7693eb9582644e5b865dba8d4447cf1': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2019-03-27T01:00:00+01:00', + 'hardware': '1', + 'location': '446ac08dd04d4eff8ac57489757b7314', + 'model': 'Tom/Floor', + 'model_id': '106-03', + 'name': 'CV Kraan Garage', + 'sensors': dict({ + 'battery': 68, + 'setpoint': 5.5, + 'temperature': 15.6, + 'temperature_difference': 0.0, + 'valve_position': 0.0, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A11', + }), + 'f1fee6043d3642a9b0a65297455f008e': dict({ + 'available': True, + 'binary_sensors': dict({ + 'low_battery': False, + }), + 'dev_class': 'thermostatic_radiator_valve', + 'firmware': '2016-10-27T02:00:00+02:00', + 'hardware': '255', + 'location': '08963fec7c53423ca5680aa4cb502c63', + 'model': 'Lisa', + 'model_id': '158-01', + 'name': 'Thermostatic Radiator Badkamer 2', + 'sensors': dict({ + 'battery': 92, + 'setpoint': 14.0, + 'temperature': 18.9, + }), + 'temperature_offset': dict({ + 'lower_bound': -2.0, + 'resolution': 0.1, + 'setpoint': 0.0, + 'upper_bound': 2.0, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670A08', + }), + 'fe799307f1624099878210aa0b9f1475': dict({ + 'binary_sensors': dict({ + 'plugwise_notification': True, + }), + 'dev_class': 'gateway', + 'firmware': '3.0.15', + 'hardware': 'AME Smile 2.0 board', + 'location': '1f9dcf83fd4e4b66b72ff787957bfe5d', + 'mac_address': '012345670001', + 'model': 'Gateway', + 'model_id': 'smile_open_therm', + 'name': 'Adam', 'notifications': dict({ 'af82e4ccf9c548528166d38e560662a4': dict({ 'warning': "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device.", }), }), - 'reboot': True, - 'smile_name': 'Adam', + 'select_regulation_mode': 'heating', + 'sensors': dict({ + 'outdoor_temperature': 7.81, + }), + 'vendor': 'Plugwise', + 'zigbee_mac_address': 'ABCD012345670101', }), }) # --- diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 5c0e3fbdd2e..7bf475086af 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock +import pytest + from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity @@ -9,32 +11,32 @@ from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) +@pytest.mark.parametrize( + ("entity_id", "expected_state"), + [ + ("binary_sensor.opentherm_secondary_boiler_state", STATE_OFF), + ("binary_sensor.opentherm_dhw_state", STATE_OFF), + ("binary_sensor.opentherm_heating", STATE_ON), + ("binary_sensor.opentherm_cooling_enabled", STATE_OFF), + ("binary_sensor.opentherm_compressor_state", STATE_ON), + ], +) async def test_anna_climate_binary_sensor_entities( - hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry + hass: HomeAssistant, + mock_smile_anna: MagicMock, + init_integration: MockConfigEntry, + entity_id: str, + expected_state: str, ) -> None: """Test creation of climate related binary_sensor entities.""" - - state = hass.states.get("binary_sensor.opentherm_secondary_boiler_state") - assert state - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.opentherm_dhw_state") - assert state - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.opentherm_heating") - assert state - assert state.state == STATE_ON - - state = hass.states.get("binary_sensor.opentherm_cooling_enabled") - assert state - assert state.state == STATE_OFF - - state = hass.states.get("binary_sensor.opentherm_compressor_state") - assert state - assert state.state == STATE_ON + state = hass.states.get(entity_id) + assert state.state == expected_state +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_climate_binary_sensor_change( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -66,8 +68,12 @@ async def test_adam_climate_binary_sensor_change( assert not state.attributes.get("other_msg") -async def test_p1_v4_binary_sensor_entity( - hass: HomeAssistant, mock_smile_p1_2: MagicMock, init_integration: MockConfigEntry +@pytest.mark.parametrize("chosen_env", ["p1v4_442_triple"], indirect=True) +@pytest.mark.parametrize( + "gateway_id", ["03e65b16e4b247a29ae0d75a78cb492e"], indirect=True +) +async def test_p1_binary_sensor_entity( + hass: HomeAssistant, mock_smile_p1: MagicMock, init_integration: MockConfigEntry ) -> None: """Test of a Smile P1 related plugwise-notification binary_sensor.""" state = hass.states.get("binary_sensor.smile_p1_plugwise_notification") diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 8368af8e5cc..7a481285be0 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -79,8 +79,12 @@ async def test_adam_climate_entity_attributes( assert state.attributes[ATTR_TARGET_TEMP_STEP] == 0.1 +@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [False], indirect=True) async def test_adam_2_climate_entity_attributes( - hass: HomeAssistant, mock_smile_adam_2: MagicMock, init_integration: MockConfigEntry + hass: HomeAssistant, + mock_smile_adam_heat_cool: MagicMock, + init_integration: MockConfigEntry, ) -> None: """Test creation of adam climate device environment.""" state = hass.states.get("climate.living_room") @@ -104,9 +108,11 @@ async def test_adam_2_climate_entity_attributes( ] +@pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_3_climate_entity_attributes( hass: HomeAssistant, - mock_smile_adam_3: MagicMock, + mock_smile_adam_heat_cool: MagicMock, init_integration: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: @@ -120,19 +126,11 @@ async def test_adam_3_climate_entity_attributes( HVACMode.AUTO, HVACMode.COOL, ] - data = mock_smile_adam_3.async_update.return_value - data.devices["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = ( - "heating" - ) - data.devices["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = ( - HVACAction.HEATING - ) - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "cooling_state" - ] = False - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "heating_state" - ] = True + data = mock_smile_adam_heat_cool.async_update.return_value + data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "heating" + data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.HEATING + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = False + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = True with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) @@ -148,19 +146,11 @@ async def test_adam_3_climate_entity_attributes( HVACMode.HEAT, ] - data = mock_smile_adam_3.async_update.return_value - data.devices["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = ( - "cooling" - ) - data.devices["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = ( - HVACAction.COOLING - ) - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "cooling_state" - ] = True - data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ - "heating_state" - ] = False + data = mock_smile_adam_heat_cool.async_update.return_value + data["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = "cooling" + data["f2bf9048bef64cc5b6d5110154e33c81"]["control_state"] = HVACAction.COOLING + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["cooling_state"] = True + data["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"]["heating_state"] = False with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) @@ -266,7 +256,7 @@ async def test_adam_climate_entity_climate_changes( async def test_adam_climate_off_mode_change( hass: HomeAssistant, - mock_smile_adam_4: MagicMock, + mock_smile_adam_jip: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test handling of user requests in adam climate device environment.""" @@ -282,9 +272,9 @@ async def test_adam_climate_off_mode_change( }, blocking=True, ) - assert mock_smile_adam_4.set_schedule_state.call_count == 1 - assert mock_smile_adam_4.set_regulation_mode.call_count == 1 - mock_smile_adam_4.set_regulation_mode.assert_called_with("heating") + assert mock_smile_adam_jip.set_schedule_state.call_count == 1 + assert mock_smile_adam_jip.set_regulation_mode.call_count == 1 + mock_smile_adam_jip.set_regulation_mode.assert_called_with("heating") state = hass.states.get("climate.kinderkamer") assert state @@ -298,9 +288,9 @@ async def test_adam_climate_off_mode_change( }, blocking=True, ) - assert mock_smile_adam_4.set_schedule_state.call_count == 1 - assert mock_smile_adam_4.set_regulation_mode.call_count == 2 - mock_smile_adam_4.set_regulation_mode.assert_called_with("off") + assert mock_smile_adam_jip.set_schedule_state.call_count == 1 + assert mock_smile_adam_jip.set_regulation_mode.call_count == 2 + mock_smile_adam_jip.set_regulation_mode.assert_called_with("off") state = hass.states.get("climate.logeerkamer") assert state @@ -314,10 +304,12 @@ async def test_adam_climate_off_mode_change( }, blocking=True, ) - assert mock_smile_adam_4.set_schedule_state.call_count == 1 - assert mock_smile_adam_4.set_regulation_mode.call_count == 2 + assert mock_smile_adam_jip.set_schedule_state.call_count == 1 + assert mock_smile_adam_jip.set_regulation_mode.call_count == 2 +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_climate_entity_attributes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -343,9 +335,11 @@ async def test_anna_climate_entity_attributes( assert state.attributes[ATTR_TARGET_TEMP_STEP] == 0.1 +@pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_cooling"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_2_climate_entity_attributes( hass: HomeAssistant, - mock_smile_anna_2: MagicMock, + mock_smile_anna: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test creation of anna climate device environment.""" @@ -362,9 +356,11 @@ async def test_anna_2_climate_entity_attributes( assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20.5 +@pytest.mark.parametrize("chosen_env", ["m_anna_heatpump_idle"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_3_climate_entity_attributes( hass: HomeAssistant, - mock_smile_anna_3: MagicMock, + mock_smile_anna: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test creation of anna climate device environment.""" @@ -378,6 +374,8 @@ async def test_anna_3_climate_entity_attributes( ] +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_climate_entity_climate_changes( hass: HomeAssistant, mock_smile_anna: MagicMock, @@ -433,7 +431,7 @@ async def test_anna_climate_entity_climate_changes( ) data = mock_smile_anna.async_update.return_value - data.devices["3cb70739631c4d17a86b8b12e8a5161b"].pop("available_schedules") + data["3cb70739631c4d17a86b8b12e8a5161b"].pop("available_schedules") with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 9e1e29f4a48..16af7065c49 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -13,8 +13,7 @@ from plugwise.exceptions import ( import pytest from homeassistant.components.plugwise.const import DEFAULT_PORT, DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -25,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -35,7 +35,7 @@ TEST_PASSWORD = "test_password" TEST_PORT = 81 TEST_USERNAME = "smile" TEST_USERNAME2 = "stretch" -MOCK_SMILE_ID = "smile12345" +TEST_SMILE_HOST = "smile12345" TEST_DISCOVERY = ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), @@ -129,7 +129,7 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smile_config_flow.connect.mock_calls) == 1 - assert result2["result"].unique_id == MOCK_SMILE_ID + assert result2["result"].unique_id == TEST_SMILE_HOST @pytest.mark.parametrize( @@ -175,7 +175,7 @@ async def test_zeroconf_flow( assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smile_config_flow.connect.mock_calls) == 1 - assert result2["result"].unique_id == MOCK_SMILE_ID + assert result2["result"].unique_id == TEST_SMILE_HOST async def test_zeroconf_flow_stretch( @@ -274,7 +274,7 @@ async def test_flow_errors( side_effect: Exception, reason: str, ) -> None: - """Test we handle invalid auth.""" + """Test we handle each exception error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, @@ -285,6 +285,7 @@ async def test_flow_errors( assert "flow_id" in result mock_smile_config_flow.connect.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, @@ -330,7 +331,7 @@ async def test_user_abort_existing_anna( CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, }, - unique_id=MOCK_SMILE_ID, + unique_id=TEST_SMILE_HOST, ) entry.add_to_hass(hass) @@ -435,3 +436,91 @@ async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: flows_in_progress = hass.config_entries.flow._handler_progress_index[DOMAIN] assert len(flows_in_progress) == 1 assert list(flows_in_progress)[0].product == "smile_open_therm" + + +async def _start_reconfigure_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + host_ip: str, +) -> ConfigFlowResult: + """Initialize a reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + + return await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], {CONF_HOST: host_ip} + ) + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_smile_adam: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_HOST) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + assert mock_config_entry.data.get(CONF_HOST) == TEST_HOST + + +async def test_reconfigure_flow_smile_mismatch( + hass: HomeAssistant, + mock_smile_adam: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow aborts on other Smile ID.""" + mock_smile_adam.smile_hostname = TEST_SMILE_HOST + + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_HOST) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_the_same_smile" + + +@pytest.mark.parametrize( + ("side_effect", "reason"), + [ + (ConnectionFailedError, "cannot_connect"), + (InvalidAuthentication, "invalid_auth"), + (InvalidSetupError, "invalid_setup"), + (InvalidXMLError, "response_error"), + (RuntimeError, "unknown"), + (UnsupportedDeviceError, "unsupported"), + ], +) +async def test_reconfigure_flow_connect_errors( + hass: HomeAssistant, + mock_smile_adam: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + reason: str, +) -> None: + """Test we handle each reconfigure exception error and recover.""" + + mock_smile_adam.connect.side_effect = side_effect + + result = await _start_reconfigure_flow(hass, mock_config_entry, TEST_HOST) + + assert result.get("type") is FlowResultType.FORM + assert result.get("errors") == {"base": reason} + assert result.get("step_id") == "reconfigure" + + mock_smile_adam.connect.side_effect = None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reconfigure_successful" + + assert mock_config_entry.data.get(CONF_HOST) == TEST_HOST diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 014003d29d0..5f1f065fa90 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -61,6 +61,8 @@ TOM = { } +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -80,6 +82,8 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) @pytest.mark.parametrize( ("side_effect", "entry_state"), [ @@ -109,6 +113,10 @@ async def test_gateway_config_entry_not_ready( assert mock_config_entry.state is entry_state +@pytest.mark.parametrize("chosen_env", ["p1v4_442_single"], indirect=True) +@pytest.mark.parametrize( + "gateway_id", ["a455b61e52394b2db5081ce025a430f3"], indirect=True +) async def test_device_in_dr( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -131,6 +139,8 @@ async def test_device_in_dr( assert device_entry.sw_version == "4.4.2" +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) @pytest.mark.parametrize( ("entitydata", "old_unique_id", "new_unique_id"), [ @@ -224,16 +234,18 @@ async def test_migrate_unique_id_relay( assert entity_migrated.unique_id == new_unique_id +@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_update_device( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_smile_adam_2: MagicMock, + mock_smile_adam_heat_cool: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: """Test a clean-up of the device_registry.""" - data = mock_smile_adam_2.async_update.return_value + data = mock_smile_adam_heat_cool.async_update.return_value mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -257,8 +269,8 @@ async def test_update_device( ) # Add a 2nd Tom/Floor - data.devices.update(TOM) - data.devices["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( + data.update(TOM) + data["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( { "secondary": [ "01234567890abcdefghijklmnopqrstu", @@ -293,10 +305,10 @@ async def test_update_device( assert "01234567890abcdefghijklmnopqrstu" in item_list # Remove the existing Tom/Floor - data.devices["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( + data["f871b8c4d63549319221e294e4f88074"]["thermostats"].update( {"secondary": ["01234567890abcdefghijklmnopqrstu"]} ) - data.devices.pop("1772a4ea304041adb83f357b751341ff") + data.pop("1772a4ea304041adb83f357b751341ff") with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index fdceb042669..4ae461d96c8 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -16,6 +16,8 @@ from homeassistant.exceptions import ServiceValidationError from tests.common import MockConfigEntry +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_number_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -25,6 +27,8 @@ async def test_anna_number_entities( assert float(state.state) == 60.0 +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_max_boiler_temp_change( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -45,19 +49,18 @@ async def test_anna_max_boiler_temp_change( ) -async def test_adam_number_entities( - hass: HomeAssistant, mock_smile_adam_2: MagicMock, init_integration: MockConfigEntry +@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [False], indirect=True) +async def test_adam_dhw_setpoint_change( + hass: HomeAssistant, + mock_smile_adam_heat_cool: MagicMock, + init_integration: MockConfigEntry, ) -> None: - """Test creation of a number.""" + """Test changing of number entities.""" state = hass.states.get("number.opentherm_domestic_hot_water_setpoint") assert state assert float(state.state) == 60.0 - -async def test_adam_dhw_setpoint_change( - hass: HomeAssistant, mock_smile_adam_2: MagicMock, init_integration: MockConfigEntry -) -> None: - """Test changing of number entities.""" await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -68,8 +71,8 @@ async def test_adam_dhw_setpoint_change( blocking=True, ) - assert mock_smile_adam_2.set_number.call_count == 1 - mock_smile_adam_2.set_number.assert_called_with( + assert mock_smile_adam_heat_cool.set_number.call_count == 1 + mock_smile_adam_heat_cool.set_number.assert_called_with( "056ee145a816487eaa69243c3280f8bf", "max_dhw_temperature", 55.0 ) diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py index 8891a88bb91..f6c4205b756 100644 --- a/tests/components/plugwise/test_select.py +++ b/tests/components/plugwise/test_select.py @@ -50,8 +50,12 @@ async def test_adam_change_select_entity( ) +@pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_select_regulation_mode( - hass: HomeAssistant, mock_smile_adam_3: MagicMock, init_integration: MockConfigEntry + hass: HomeAssistant, + mock_smile_adam_heat_cool: MagicMock, + init_integration: MockConfigEntry, ) -> None: """Test a regulation_mode select. @@ -73,8 +77,8 @@ async def test_adam_select_regulation_mode( }, blocking=True, ) - assert mock_smile_adam_3.set_select.call_count == 1 - mock_smile_adam_3.set_select.assert_called_with( + assert mock_smile_adam_heat_cool.set_select.call_count == 1 + mock_smile_adam_heat_cool.set_select.assert_called_with( "select_regulation_mode", "bc93488efab249e5bc54fd7e175a6f91", "heating", @@ -91,6 +95,8 @@ async def test_legacy_anna_select_entities( assert not hass.states.get("select.anna_thermostat_schedule") +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_select_unavailable_regulation_mode( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index f10f3f00933..c6c6c6cc284 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -7,8 +7,8 @@ import pytest from homeassistant.components.plugwise.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -import homeassistant.helpers.entity_registry as er from tests.common import MockConfigEntry @@ -41,7 +41,9 @@ async def test_adam_climate_sensor_entities( async def test_adam_climate_sensor_entity_2( - hass: HomeAssistant, mock_smile_adam_4: MagicMock, init_integration: MockConfigEntry + hass: HomeAssistant, + mock_smile_adam_jip: MagicMock, + init_integration: MockConfigEntry, ) -> None: """Test creation of climate related sensor entities.""" state = hass.states.get("sensor.woonkamer_humidity") @@ -52,7 +54,7 @@ async def test_adam_climate_sensor_entity_2( async def test_unique_id_migration_humidity( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_smile_adam_4: MagicMock, + mock_smile_adam_jip: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test unique ID migration of -relative_humidity to -humidity.""" @@ -92,6 +94,8 @@ async def test_unique_id_migration_humidity( assert entity_entry.unique_id == "f61f1a2535f54f52ad006a3d18e459ca-battery" +@pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) +@pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_anna_as_smt_climate_sensor_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -113,6 +117,10 @@ async def test_anna_as_smt_climate_sensor_entities( assert float(state.state) == 86.0 +@pytest.mark.parametrize("chosen_env", ["p1v4_442_single"], indirect=True) +@pytest.mark.parametrize( + "gateway_id", ["a455b61e52394b2db5081ce025a430f3"], indirect=True +) async def test_p1_dsmr_sensor_entities( hass: HomeAssistant, mock_smile_p1: MagicMock, init_integration: MockConfigEntry ) -> None: @@ -137,11 +145,15 @@ async def test_p1_dsmr_sensor_entities( assert not state +@pytest.mark.parametrize("chosen_env", ["p1v4_442_triple"], indirect=True) +@pytest.mark.parametrize( + "gateway_id", ["03e65b16e4b247a29ae0d75a78cb492e"], indirect=True +) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_p1_3ph_dsmr_sensor_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_smile_p1_2: MagicMock, + mock_smile_p1: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test creation of power related sensor entities.""" @@ -163,10 +175,14 @@ async def test_p1_3ph_dsmr_sensor_entities( assert float(state.state) == 233.2 +@pytest.mark.parametrize("chosen_env", ["p1v4_442_triple"], indirect=True) +@pytest.mark.parametrize( + "gateway_id", ["03e65b16e4b247a29ae0d75a78cb492e"], indirect=True +) async def test_p1_3ph_dsmr_sensor_disabled_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, - mock_smile_p1_2: MagicMock, + mock_smile_p1: MagicMock, init_integration: MockConfigEntry, ) -> None: """Test disabled power related sensor entities intent.""" diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index fa8a8a434e7..003c47ed1f4 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index bd1e3cfac29..ea003af86c7 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( # noqa: SLF001 + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], diff --git a/tests/components/poolsense/snapshots/test_binary_sensor.ambr b/tests/components/poolsense/snapshots/test_binary_sensor.ambr index 8a6d39332d4..b3d99b95308 100644 --- a/tests/components/poolsense/snapshots/test_binary_sensor.ambr +++ b/tests/components/poolsense/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/poolsense/snapshots/test_sensor.ambr b/tests/components/poolsense/snapshots/test_sensor.ambr index 9029f1f24aa..c0066ba9396 100644 --- a/tests/components/poolsense/snapshots/test_sensor.ambr +++ b/tests/components/poolsense/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,6 +56,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -103,6 +105,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -151,6 +154,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -199,6 +203,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -247,6 +252,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -295,6 +301,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -342,6 +349,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -389,6 +397,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/powerfox/conftest.py b/tests/components/powerfox/conftest.py index 14ccc5996e5..1d930394254 100644 --- a/tests/components/powerfox/conftest.py +++ b/tests/components/powerfox/conftest.py @@ -4,7 +4,7 @@ from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch -from powerfox import Device, DeviceType, PowerMeter, WaterMeter +from powerfox import Device, DeviceType, HeatMeter, PowerMeter, WaterMeter import pytest from homeassistant.components.powerfox.const import DOMAIN @@ -53,6 +53,14 @@ def mock_powerfox_client() -> Generator[AsyncMock]: type=DeviceType.COLD_WATER_METER, name="Wateropti", ), + Device( + id="9x9x1f12xx5x", + date_added=datetime(2024, 11, 26, 9, 22, 35, tzinfo=UTC), + main_device=False, + bidirectional=False, + type=DeviceType.HEAT_METER, + name="Heatopti", + ), ] client.device.side_effect = [ PowerMeter( @@ -70,6 +78,14 @@ def mock_powerfox_client() -> Generator[AsyncMock]: cold_water=1111.111, warm_water=0.0, ), + HeatMeter( + outdated=False, + timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), + total_energy=1111.111, + delta_energy=111, + total_volume=1111.111, + delta_volume=0.111, + ), ] yield client diff --git a/tests/components/powerfox/snapshots/test_diagnostics.ambr b/tests/components/powerfox/snapshots/test_diagnostics.ambr index 781e7b8c0d5..d749a1b5b60 100644 --- a/tests/components/powerfox/snapshots/test_diagnostics.ambr +++ b/tests/components/powerfox/snapshots/test_diagnostics.ambr @@ -21,6 +21,16 @@ 'warm_water': 0.0, }), }), + dict({ + 'heat_meter': dict({ + 'delta_energy': 111, + 'delta_volume': 0.111, + 'outdated': False, + 'timestamp': '2024-11-26 10:48:51', + 'total_energy': 1111.111, + 'total_volume': 1111.111, + }), + }), ]), }) # --- diff --git a/tests/components/powerfox/snapshots/test_sensor.ambr b/tests/components/powerfox/snapshots/test_sensor.ambr index dda162d4eeb..bae306ccabc 100644 --- a/tests/components/powerfox/snapshots/test_sensor.ambr +++ b/tests/components/powerfox/snapshots/test_sensor.ambr @@ -1,4 +1,209 @@ # serializer version: 1 +# name: test_all_sensors[sensor.heatopti_delta_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heatopti_delta_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Delta energy', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heat_delta_energy', + 'unique_id': '9x9x1f12xx5x_heat_delta_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.heatopti_delta_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heatopti Delta energy', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heatopti_delta_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '111', + }) +# --- +# name: test_all_sensors[sensor.heatopti_delta_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heatopti_delta_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Delta volume', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heat_delta_volume', + 'unique_id': '9x9x1f12xx5x_heat_delta_volume', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.heatopti_delta_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Heatopti Delta volume', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heatopti_delta_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.111', + }) +# --- +# name: test_all_sensors[sensor.heatopti_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heatopti_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heat_total_energy', + 'unique_id': '9x9x1f12xx5x_heat_total_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.heatopti_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heatopti Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heatopti_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1111.111', + }) +# --- +# name: test_all_sensors[sensor.heatopti_total_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heatopti_total_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total volume', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heat_total_volume', + 'unique_id': '9x9x1f12xx5x_heat_total_volume', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.heatopti_total_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Heatopti Total volume', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heatopti_total_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1111.111', + }) +# --- # name: test_all_sensors[sensor.poweropti_energy_return-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -8,6 +213,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +265,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +317,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +369,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -212,6 +421,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -263,6 +473,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -314,6 +525,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/powerfox/test_config_flow.py b/tests/components/powerfox/test_config_flow.py index a38f316faf3..377ae9c622c 100644 --- a/tests/components/powerfox/test_config_flow.py +++ b/tests/components/powerfox/test_config_flow.py @@ -5,18 +5,18 @@ from unittest.mock import AsyncMock, patch from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError import pytest -from homeassistant.components import zeroconf from homeassistant.components.powerfox.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from . import MOCK_DIRECT_HOST from tests.common import MockConfigEntry -MOCK_ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=MOCK_DIRECT_HOST, ip_addresses=[MOCK_DIRECT_HOST], hostname="powerfox.local", diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 1ff1470f81c..ab5034de637 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -11,13 +11,13 @@ from tesla_powerwall import ( ) from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -import homeassistant.util.dt as dt_util +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.util import dt as dt_util from .mocks import ( MOCK_GATEWAY_DIN, @@ -161,7 +161,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="any", @@ -188,7 +188,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="00GGX", @@ -230,7 +230,7 @@ async def test_dhcp_discovery_manual_configure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="any", @@ -272,7 +272,7 @@ async def test_dhcp_discovery_auto_configure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="00GGX", @@ -316,7 +316,7 @@ async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname="00GGX", @@ -394,7 +394,7 @@ async def test_dhcp_discovery_update_ip_address(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), @@ -431,7 +431,7 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_fails( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), @@ -468,7 +468,7 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_successful( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), @@ -503,7 +503,7 @@ async def test_dhcp_discovery_updates_unique_id(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), @@ -542,7 +542,7 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), @@ -581,7 +581,7 @@ async def test_discovered_wifi_does_not_update_ip_if_is_still_online( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.5", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), @@ -630,7 +630,7 @@ async def test_discovered_wifi_does_not_update_ip_online_but_access_denied( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.5", macaddress="aabbcceeddff", hostname=MOCK_GATEWAY_DIN.lower(), diff --git a/tests/components/powerwall/test_init.py b/tests/components/powerwall/test_init.py index e271cde0fc4..dd70dbb7c65 100644 --- a/tests/components/powerwall/test_init.py +++ b/tests/components/powerwall/test_init.py @@ -1,17 +1,23 @@ """Tests for the PowerwallDataManager.""" import datetime -from unittest.mock import patch +from http.cookies import Morsel +from unittest.mock import MagicMock, patch +from aiohttp import CookieJar from tesla_powerwall import AccessDeniedError, LoginResponse -from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.components.powerwall.const import ( + AUTH_COOKIE_KEY, + CONFIG_ENTRY_COOKIE, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow -from .mocks import _mock_powerwall_with_fixtures +from .mocks import MOCK_GATEWAY_DIN, _mock_powerwall_with_fixtures from tests.common import MockConfigEntry, async_fire_time_changed @@ -37,7 +43,11 @@ async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant) mock_powerwall.is_authenticated.return_value = True config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "password"} + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "password", + }, ) config_entry.add_to_hass(hass) with ( @@ -72,3 +82,226 @@ async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant) assert len(flows) == 1 reauth_flow = flows[0] assert reauth_flow["context"]["source"] == "reauth" + + +async def test_init_uses_cookie_if_present(hass: HomeAssistant) -> None: + """Tests if the init will use the auth cookie if present. + + If the cookie is present, the login step will be skipped and info will be fetched directly (see _login_and_fetch_base_info). + """ + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "somepassword", + CONFIG_ENTRY_COOKIE: "somecookie", + }, + ) + config_entry.add_to_hass(hass) + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert not mock_powerwall.login.called + assert mock_powerwall.get_gateway_din.called + + +async def test_init_uses_password_if_no_cookies(hass: HomeAssistant) -> None: + """Tests if the init will use the password if no auth cookie present.""" + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "somepassword", + }, + ) + config_entry.add_to_hass(hass) + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_powerwall.login.assert_called_with("somepassword") + assert mock_powerwall.get_charge.called + + +async def test_init_saves_the_cookie(hass: HomeAssistant) -> None: + """Tests that the cookie is properly saved.""" + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + mock_jar = MagicMock(CookieJar) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "somepassword", + }, + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), + patch("homeassistant.components.powerwall.CookieJar", return_value=mock_jar), + ): + auth_cookie = Morsel() + auth_cookie.set(AUTH_COOKIE_KEY, "somecookie", "somecookie") + mock_jar.__iter__.return_value = [auth_cookie] + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.data[CONFIG_ENTRY_COOKIE] == "somecookie" + + +async def test_retry_ignores_cookie(hass: HomeAssistant) -> None: + """Tests that retrying uses the password instead.""" + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "somepassword", + CONFIG_ENTRY_COOKIE: "somecookie", + }, + ) + config_entry.add_to_hass(hass) + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert not mock_powerwall.login.called + assert mock_powerwall.get_gateway_din.called + + mock_powerwall.login.reset_mock() + mock_powerwall.get_charge.reset_mock() + + mock_powerwall.get_charge.side_effect = [AccessDeniedError("test"), 90.0] + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) + await hass.async_block_till_done() + + mock_powerwall.login.assert_called_with("somepassword") + assert mock_powerwall.get_charge.call_count == 2 + + +async def test_reauth_ignores_and_clears_cookie(hass: HomeAssistant) -> None: + """Tests that the reauth flow uses password and clears the cookie.""" + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "somepassword", + CONFIG_ENTRY_COOKIE: "somecookie", + }, + ) + config_entry.add_to_hass(hass) + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_powerwall.login.reset_mock() + mock_powerwall.get_charge.reset_mock() + + mock_powerwall.get_charge.side_effect = [ + AccessDeniedError("test"), + AccessDeniedError("test"), + ] + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) + await hass.async_block_till_done() + + mock_powerwall.login.assert_called_with("somepassword") + assert mock_powerwall.get_charge.call_count == 2 + + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 1 + reauth_flow = flows[0] + assert reauth_flow["context"]["source"] == "reauth" + + mock_powerwall.login.reset_mock() + assert config_entry.data[CONFIG_ENTRY_COOKIE] is not None + + await hass.config_entries.flow.async_configure( + reauth_flow["flow_id"], {CONF_PASSWORD: "somepassword"} + ) + + mock_powerwall.login.assert_called_with("somepassword") + assert config_entry.data[CONFIG_ENTRY_COOKIE] is None + + +async def test_init_retries_with_password(hass: HomeAssistant) -> None: + """Tests that the init retries with password if cookie fails.""" + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "somepassword", + CONFIG_ENTRY_COOKIE: "somecookie", + }, + ) + config_entry.add_to_hass(hass) + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), + ): + mock_powerwall.get_gateway_din.side_effect = [ + AccessDeniedError("get_gateway_din"), + MOCK_GATEWAY_DIN, + ] + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_powerwall.login.assert_called_with("somepassword") + assert mock_powerwall.get_gateway_din.call_count == 2 diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index fa2d986d12a..9b533304fbc 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .mocks import MOCK_GATEWAY_DIN, _mock_powerwall_with_fixtures diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 84314b7b22c..e724a9e5cab 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -5,7 +5,6 @@ from functools import lru_cache import logging import os from pathlib import Path -import sys from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory @@ -35,7 +34,7 @@ from homeassistant.components.profiler.const import DOMAIN from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -71,9 +70,6 @@ async def test_basic_usage(hass: HomeAssistant, tmp_path: Path) -> None: await hass.async_block_till_done() -@pytest.mark.skipif( - sys.version_info >= (3, 13), reason="not yet available on Python 3.13" -) async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None: """Test we can setup and the service is registered.""" test_dir = tmp_path / "profiles" @@ -105,24 +101,6 @@ async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None: await hass.async_block_till_done() -@pytest.mark.skipif(sys.version_info < (3, 13), reason="still works on python 3.12") -async def test_memory_usage_py313(hass: HomeAssistant, tmp_path: Path) -> None: - """Test raise an error on python3.13.""" - entry = MockConfigEntry(domain=DOMAIN) - entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert hass.services.has_service(DOMAIN, SERVICE_MEMORY) - with pytest.raises( - HomeAssistantError, - match="Memory profiling is not supported on Python 3.13. Please use Python 3.12.", - ): - await hass.services.async_call( - DOMAIN, SERVICE_MEMORY, {CONF_SECONDS: 0.000001}, blocking=True - ) - - async def test_object_growth_logging( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 043a9cc4389..bbd58619b12 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -541,8 +541,7 @@ async def test_view_empty_namespace( assert "# HELP python_info Python platform information" in body assert ( - "# HELP python_gc_objects_collected_total " - "Objects collected during gc" in body + "# HELP python_gc_objects_collected_total Objects collected during gc" in body ) EntityMetric( @@ -569,8 +568,7 @@ async def test_view_default_namespace( assert "# HELP python_info Python platform information" in body assert ( - "# HELP python_gc_objects_collected_total " - "Objects collected during gc" in body + "# HELP python_gc_objects_collected_total Objects collected during gc" in body ) EntityMetric( diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index eeb181e0670..22a546e6abe 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -15,8 +15,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.issue_registry as ir +from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.util import slugify from tests.common import MockConfigEntry diff --git a/tests/components/prusalink/test_binary_sensor.py b/tests/components/prusalink/test_binary_sensor.py index c39b15471c6..474a4e265d1 100644 --- a/tests/components/prusalink/test_binary_sensor.py +++ b/tests/components/prusalink/test_binary_sensor.py @@ -1,6 +1,6 @@ """Test Prusalink sensors.""" -from unittest.mock import PropertyMock, patch +from unittest.mock import patch import pytest @@ -12,16 +12,13 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) def setup_binary_sensor_platform_only(): """Only setup sensor platform.""" - with ( - patch("homeassistant.components.prusalink.PLATFORMS", [Platform.BINARY_SENSOR]), - patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ), + with patch( + "homeassistant.components.prusalink.PLATFORMS", [Platform.BINARY_SENSOR] ): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors_no_job( hass: HomeAssistant, mock_config_entry, mock_api ) -> None: diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index c0693626600..ead56f6493d 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -1,7 +1,7 @@ """Test Prusalink sensors.""" from datetime import UTC, datetime -from unittest.mock import PropertyMock, patch +from unittest.mock import patch import pytest @@ -27,16 +27,11 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) def setup_sensor_platform_only(): """Only setup sensor platform.""" - with ( - patch("homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR]), - patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ), - ): + with patch("homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR]): yield +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) -> None: """Test sensors while no job active.""" assert await async_setup_component(hass, "prusalink", {}) @@ -140,6 +135,7 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_idle_job_mk3( hass: HomeAssistant, mock_config_entry, @@ -248,6 +244,7 @@ async def test_sensors_idle_job_mk3( assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensors_active_job( hass: HomeAssistant, mock_config_entry, diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 4e0505a8644..a4e6b039a92 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -24,7 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.util import location +from homeassistant.util import location as location_util from tests.common import MockConfigEntry @@ -64,7 +64,7 @@ MOCK_TCP_PORT = 997 MOCK_AUTO = {"Config Mode": "Auto Discover"} MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} -MOCK_LOCATION = location.LocationInfo( +MOCK_LOCATION = location_util.LocationInfo( "0.0.0.0", "US", "USD", @@ -83,7 +83,8 @@ MOCK_LOCATION = location.LocationInfo( def location_info_fixture(): """Mock location info.""" with patch( - "homeassistant.components.ps4.config_flow.location.async_detect_location_info", + "homeassistant.components.ps4." + "config_flow.location_util.async_detect_location_info", return_value=MOCK_LOCATION, ): yield @@ -359,7 +360,8 @@ async def test_0_pin(hass: HomeAssistant) -> None: "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ), patch( - "homeassistant.components.ps4.config_flow.location.async_detect_location_info", + "homeassistant.components.ps4." + "config_flow.location_util.async_detect_location_info", return_value=MOCK_LOCATION, ), ): diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 24d45fee5b9..4cde723a28f 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -29,9 +29,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util import location +from homeassistant.util import location as location_util from tests.common import MockConfigEntry @@ -59,7 +59,7 @@ MOCK_ENTRY_ID = "SomeID" MOCK_CONFIG = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY_ID) -MOCK_LOCATION = location.LocationInfo( +MOCK_LOCATION = location_util.LocationInfo( "0.0.0.0", "US", "USD", @@ -81,8 +81,6 @@ MOCK_DEVICE_VERSION_1 = { MOCK_DATA_VERSION_1 = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE_VERSION_1]} -MOCK_DEVICE_ID = "somedeviceid" - MOCK_ENTRY_VERSION_1 = MockConfigEntry( domain=DOMAIN, data=MOCK_DATA_VERSION_1, entry_id=MOCK_ENTRY_ID, version=1 ) @@ -142,20 +140,26 @@ async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: async def test_config_flow_entry_migrate( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test that config flow entry is migrated correctly.""" # Start with the config entry at Version 1. manager = hass.config_entries mock_entry = MOCK_ENTRY_VERSION_1 mock_entry.add_to_manager(manager) + mock_device_entry = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) mock_entity_id = f"media_player.ps4_{MOCK_UNIQUE_ID}" mock_e_entry = entity_registry.async_get_or_create( "media_player", "ps4", MOCK_UNIQUE_ID, config_entry=mock_entry, - device_id=MOCK_DEVICE_ID, + device_id=mock_device_entry.id, ) assert len(entity_registry.entities) == 1 assert mock_e_entry.entity_id == mock_entity_id @@ -181,7 +185,7 @@ async def test_config_flow_entry_migrate( # Test that entity_id remains the same. assert mock_entity.entity_id == mock_entity_id - assert mock_entity.device_id == MOCK_DEVICE_ID + assert mock_entity.device_id == mock_device_entry.id # Test that last four of credentials is appended to the unique_id. assert mock_entity.unique_id == f"{MOCK_UNIQUE_ID}_{MOCK_CREDS[-4:]}" diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 4305dab2236..96704e900dd 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -5,12 +5,12 @@ from unittest.mock import MagicMock from gridnet import GridNetConnectionError -from homeassistant.components import zeroconf from homeassistant.components.pure_energie.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo async def test_full_user_flow_implementation( @@ -48,7 +48,7 @@ async def test_full_zeroconf_flow_implementationn( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -104,7 +104,7 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", diff --git a/tests/components/pyload/conftest.py b/tests/components/pyload/conftest.py index c0f181396ab..46144771cc1 100644 --- a/tests/components/pyload/conftest.py +++ b/tests/components/pyload/conftest.py @@ -9,16 +9,12 @@ import pytest from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN from homeassistant.const import ( CONF_HOST, - CONF_MONITORED_VARIABLES, - CONF_NAME, CONF_PASSWORD, - CONF_PLATFORM, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.helpers.typing import ConfigType from tests.common import MockConfigEntry @@ -31,16 +27,6 @@ USER_INPUT = { CONF_VERIFY_SSL: False, } -YAML_INPUT = { - CONF_HOST: "pyload.local", - CONF_MONITORED_VARIABLES: ["speed"], - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_PLATFORM: "pyload", - CONF_PORT: 8000, - CONF_SSL: True, - CONF_USERNAME: "test-username", -} REAUTH_INPUT = { CONF_PASSWORD: "new-password", CONF_USERNAME: "new-username", @@ -65,12 +51,6 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup_entry -@pytest.fixture -def pyload_config() -> ConfigType: - """Mock pyload configuration entry.""" - return {"sensor": YAML_INPUT} - - @pytest.fixture def mock_pyloadapi() -> Generator[MagicMock]: """Mock PyLoadAPI.""" diff --git a/tests/components/pyload/snapshots/test_button.ambr b/tests/components/pyload/snapshots/test_button.ambr index bf1e1f59c98..57a0358da42 100644 --- a/tests/components/pyload/snapshots/test_button.ambr +++ b/tests/components/pyload/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index 69d0387fc8f..25abe62017d 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -106,6 +108,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -160,6 +163,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -216,6 +220,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -266,6 +271,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -316,6 +322,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -364,6 +371,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -418,6 +426,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -474,6 +483,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -524,6 +534,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -574,6 +585,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -622,6 +634,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -676,6 +689,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -732,6 +746,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -782,6 +797,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -832,6 +848,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -880,6 +897,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -934,6 +952,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -990,6 +1009,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr index 0fcc45f8586..479013b09e4 100644 --- a/tests/components/pyload/snapshots/test_switch.ambr +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/pyload/test_config_flow.py b/tests/components/pyload/test_config_flow.py index 5ada856d78e..492e4a4b652 100644 --- a/tests/components/pyload/test_config_flow.py +++ b/tests/components/pyload/test_config_flow.py @@ -6,11 +6,11 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from homeassistant.components.pyload.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import NEW_INPUT, REAUTH_INPUT, USER_INPUT, YAML_INPUT +from .conftest import NEW_INPUT, REAUTH_INPUT, USER_INPUT from tests.common import MockConfigEntry @@ -103,69 +103,6 @@ async def test_flow_user_already_configured( assert result["reason"] == "already_configured" -async def test_flow_import( - hass: HomeAssistant, - mock_pyloadapi: AsyncMock, -) -> None: - """Test that we can import a YAML config.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=YAML_INPUT, - ) - - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" - assert result["data"] == USER_INPUT - - -async def test_flow_import_already_configured( - hass: HomeAssistant, config_entry: MockConfigEntry, mock_pyloadapi: AsyncMock -) -> None: - """Test we abort import data set when entry is already configured.""" - - config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=YAML_INPUT, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.parametrize( - ("exception", "reason"), - [ - (InvalidAuth, "invalid_auth"), - (CannotConnect, "cannot_connect"), - (ParserError, "cannot_connect"), - (ValueError, "unknown"), - ], -) -async def test_flow_import_errors( - hass: HomeAssistant, - config_entry: MockConfigEntry, - mock_pyloadapi: AsyncMock, - exception: Exception, - reason: str, -) -> None: - """Test we abort import data set when entry is already configured.""" - - mock_pyloadapi.login.side_effect = exception - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=YAML_INPUT, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason - - async def test_reauth( hass: HomeAssistant, config_entry: MockConfigEntry, diff --git a/tests/components/pyload/test_sensor.py b/tests/components/pyload/test_sensor.py index 8c194a111ea..33ad3435083 100644 --- a/tests/components/pyload/test_sensor.py +++ b/tests/components/pyload/test_sensor.py @@ -8,15 +8,11 @@ from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.pyload.const import DOMAIN from homeassistant.components.pyload.coordinator import SCAN_INTERVAL -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.helpers.typing import ConfigType -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -101,64 +97,6 @@ async def test_sensor_invalid_auth( ) -async def test_platform_setup_triggers_import_flow( - hass: HomeAssistant, - pyload_config: ConfigType, - mock_setup_entry: AsyncMock, - mock_pyloadapi: AsyncMock, -) -> None: - """Test if an issue is created when attempting setup from yaml config.""" - - assert await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize( - ("exception", "reason"), - [ - (InvalidAuth, "invalid_auth"), - (CannotConnect, "cannot_connect"), - (ParserError, "cannot_connect"), - (ValueError, "unknown"), - ], -) -async def test_deprecated_yaml_import_issue( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - pyload_config: ConfigType, - mock_pyloadapi: AsyncMock, - exception: Exception, - reason: str, -) -> None: - """Test an issue is created when attempting setup from yaml config and an error happens.""" - - mock_pyloadapi.login.side_effect = exception - await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) - await hass.async_block_till_done() - - assert issue_registry.async_get_issue( - domain=DOMAIN, issue_id=f"deprecated_yaml_import_issue_{reason}" - ) - - -async def test_deprecated_yaml( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - pyload_config: ConfigType, - mock_pyloadapi: AsyncMock, -) -> None: - """Test an issue is created when we import from yaml config.""" - - await async_setup_component(hass, SENSOR_DOMAIN, pyload_config) - await hass.async_block_till_done() - - assert issue_registry.async_get_issue( - domain=HOMEASSISTANT_DOMAIN, issue_id=f"deprecated_yaml_{DOMAIN}" - ) - - async def test_pyload_pre_0_5_0( hass: HomeAssistant, config_entry: MockConfigEntry, diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 2d151b4b81e..14229e83662 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -711,4 +711,4 @@ async def test_no_other_imports_allowed( source = "import sys" hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done(wait_background_tasks=True) - assert "Error executing script: Not allowed to import sys" in caplog.text + assert "ImportError: Not allowed to import sys" in caplog.text diff --git a/tests/components/qbus/__init__.py b/tests/components/qbus/__init__.py new file mode 100644 index 00000000000..e8c002d1ed9 --- /dev/null +++ b/tests/components/qbus/__init__.py @@ -0,0 +1 @@ +"""Tests for the Qbus integration.""" diff --git a/tests/components/qbus/conftest.py b/tests/components/qbus/conftest.py new file mode 100644 index 00000000000..8268d091bda --- /dev/null +++ b/tests/components/qbus/conftest.py @@ -0,0 +1,33 @@ +"""Test fixtures for qbus.""" + +import pytest + +from homeassistant.components.qbus.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonObjectType + +from .const import FIXTURE_PAYLOAD_CONFIG + +from tests.common import MockConfigEntry, load_json_object_fixture + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return the default mocked config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="000001", + data={ + CONF_ID: "UL1", + CONF_SERIAL_NUMBER: "000001", + }, + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture +def payload_config() -> JsonObjectType: + """Return the config topic payload.""" + return load_json_object_fixture(FIXTURE_PAYLOAD_CONFIG, DOMAIN) diff --git a/tests/components/qbus/const.py b/tests/components/qbus/const.py new file mode 100644 index 00000000000..408ef59d5b1 --- /dev/null +++ b/tests/components/qbus/const.py @@ -0,0 +1,4 @@ +"""Define const for unit tests.""" + +FIXTURE_PAYLOAD_CONFIG = "payload_config.json" +TOPIC_CONFIG = "cloudapp/QBUSMQTTGW/config" diff --git a/tests/components/qbus/fixtures/payload_config.json b/tests/components/qbus/fixtures/payload_config.json new file mode 100644 index 00000000000..2ee38a9927e --- /dev/null +++ b/tests/components/qbus/fixtures/payload_config.json @@ -0,0 +1,49 @@ +{ + "app": "abc", + "devices": [ + { + "id": "UL1", + "ip": "192.168.1.123", + "mac": "001122334455", + "name": "", + "serialNr": "000001", + "type": "Qbus", + "version": "3.14.0", + "properties": { + "connectable": { + "read": true, + "type": "boolean", + "write": false + }, + "connected": { + "read": true, + "type": "boolean", + "write": false + } + }, + "functionBlocks": [ + { + "id": "UL10", + "location": "Living", + "locationId": 0, + "name": "LIVING", + "originalName": "LIVING", + "refId": "000001/10", + "type": "onoff", + "variant": [null], + "actions": { + "off": null, + "on": null + }, + "properties": { + "value": { + "read": true, + "type": "boolean", + "write": true + } + } + } + ] + } + ] +} diff --git a/tests/components/qbus/test_config_flow.py b/tests/components/qbus/test_config_flow.py new file mode 100644 index 00000000000..4f94f2bb277 --- /dev/null +++ b/tests/components/qbus/test_config_flow.py @@ -0,0 +1,202 @@ +"""Test config flow.""" + +import json +import time +from unittest.mock import patch + +import pytest +from qbusmqttapi.discovery import QbusDiscovery + +from homeassistant.components.qbus.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.components.qbus.coordinator import QbusConfigCoordinator +from homeassistant.config_entries import SOURCE_MQTT, SOURCE_USER +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo +from homeassistant.util.json import JsonObjectType + +from .const import TOPIC_CONFIG + +_PAYLOAD_DEVICE_STATE = '{"id":"UL1","properties":{"connected":true},"type":"event"}' + + +async def test_step_discovery_confirm_create_entry( + hass: HomeAssistant, payload_config: JsonObjectType +) -> None: + """Test mqtt confirm step and entry creation.""" + discovery = MqttServiceInfo( + subscribed_topic="cloudapp/QBUSMQTTGW/+/state", + topic="cloudapp/QBUSMQTTGW/UL1/state", + payload=_PAYLOAD_DEVICE_STATE, + qos=0, + retain=False, + timestamp=time.time(), + ) + + with ( + patch.object( + QbusConfigCoordinator, + "async_get_or_request_config", + return_value=QbusDiscovery(payload_config), + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("data") == { + CONF_ID: "UL1", + CONF_SERIAL_NUMBER: "000001", + } + assert result.get("result").unique_id == "000001" + + +@pytest.mark.parametrize( + ("topic", "payload"), + [ + ("cloudapp/QBUSMQTTGW/state", b""), + ("invalid/topic", b"{}"), + ], +) +async def test_step_mqtt_invalid( + hass: HomeAssistant, topic: str, payload: bytes +) -> None: + """Test mqtt discovery with empty payload.""" + discovery = MqttServiceInfo( + subscribed_topic=topic, + topic=topic, + payload=payload, + qos=0, + retain=False, + timestamp=time.time(), + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "invalid_discovery_info" + + +@pytest.mark.parametrize( + ("payload", "mqtt_publish"), + [ + ('{ "online": true }', True), + ('{ "online": false }', False), + ], +) +async def test_handle_gateway_topic_when_online( + hass: HomeAssistant, payload: str, mqtt_publish: bool +) -> None: + """Test handling of gateway topic with payload indicating online.""" + discovery = MqttServiceInfo( + subscribed_topic="cloudapp/QBUSMQTTGW/state", + topic="cloudapp/QBUSMQTTGW/state", + payload=payload, + qos=0, + retain=False, + timestamp=time.time(), + ) + + with ( + patch("homeassistant.components.mqtt.client.async_publish") as mock_publish, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery + ) + + assert mock_publish.called is mqtt_publish + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "discovery_in_progress" + + +async def test_handle_config_topic( + hass: HomeAssistant, payload_config: JsonObjectType +) -> None: + """Test handling of config topic.""" + + discovery = MqttServiceInfo( + subscribed_topic=TOPIC_CONFIG, + topic=TOPIC_CONFIG, + payload=json.dumps(payload_config), + qos=0, + retain=False, + timestamp=time.time(), + ) + + with ( + patch("homeassistant.components.mqtt.client.async_publish") as mock_publish, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery + ) + + assert mock_publish.called + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "discovery_in_progress" + + +async def test_handle_device_topic_missing_config(hass: HomeAssistant) -> None: + """Test handling of device topic when config is missing.""" + discovery = MqttServiceInfo( + subscribed_topic="cloudapp/QBUSMQTTGW/+/state", + topic="cloudapp/QBUSMQTTGW/UL1/state", + payload=_PAYLOAD_DEVICE_STATE, + qos=0, + retain=False, + timestamp=time.time(), + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "invalid_discovery_info" + + +async def test_handle_device_topic_device_not_found( + hass: HomeAssistant, payload_config: JsonObjectType +) -> None: + """Test handling of device topic when device is not found.""" + discovery = MqttServiceInfo( + subscribed_topic="cloudapp/QBUSMQTTGW/+/state", + topic="cloudapp/QBUSMQTTGW/UL2/state", + payload='{"id":"UL2","properties":{"connected":true},"type":"event"}', + qos=0, + retain=False, + timestamp=time.time(), + ) + + with patch.object( + QbusConfigCoordinator, + "async_get_or_request_config", + return_value=QbusDiscovery(payload_config), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_MQTT}, data=discovery + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "invalid_discovery_info" + + +async def test_step_user_not_supported(hass: HomeAssistant) -> None: + """Test user step, which should abort.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "not_supported" diff --git a/tests/components/qbus/test_switch.py b/tests/components/qbus/test_switch.py new file mode 100644 index 00000000000..83bb667e4eb --- /dev/null +++ b/tests/components/qbus/test_switch.py @@ -0,0 +1,84 @@ +"""Test Qbus switch entities.""" + +import json + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonObjectType + +from .const import TOPIC_CONFIG + +from tests.common import MockConfigEntry, async_fire_mqtt_message +from tests.typing import MqttMockHAClient + +_PAYLOAD_SWITCH_STATE_ON = '{"id":"UL10","properties":{"value":true},"type":"state"}' +_PAYLOAD_SWITCH_STATE_OFF = '{"id":"UL10","properties":{"value":false},"type":"state"}' +_PAYLOAD_SWITCH_SET_STATE_ON = ( + '{"id": "UL10", "type": "state", "properties": {"value": true}}' +) +_PAYLOAD_SWITCH_SET_STATE_OFF = ( + '{"id": "UL10", "type": "state", "properties": {"value": false}}' +) + +_TOPIC_SWITCH_STATE = "cloudapp/QBUSMQTTGW/UL1/UL10/state" +_TOPIC_SWITCH_SET_STATE = "cloudapp/QBUSMQTTGW/UL1/UL10/setState" + +_SWITCH_ENTITY_ID = "switch.living" + + +async def test_switch_turn_on_off( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + mock_config_entry: MockConfigEntry, + payload_config: JsonObjectType, +) -> None: + """Test turning on and off.""" + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, TOPIC_CONFIG, json.dumps(payload_config)) + await hass.async_block_till_done() + + # Switch ON + mqtt_mock.reset_mock() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: _SWITCH_ENTITY_ID}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + _TOPIC_SWITCH_SET_STATE, _PAYLOAD_SWITCH_SET_STATE_ON, 0, False + ) + + # Simulate response + async_fire_mqtt_message(hass, _TOPIC_SWITCH_STATE, _PAYLOAD_SWITCH_STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get(_SWITCH_ENTITY_ID).state == STATE_ON + + # Switch OFF + mqtt_mock.reset_mock() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: _SWITCH_ENTITY_ID}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + _TOPIC_SWITCH_SET_STATE, _PAYLOAD_SWITCH_SET_STATE_OFF, 0, False + ) + + # Simulate response + async_fire_mqtt_message(hass, _TOPIC_SWITCH_STATE, _PAYLOAD_SWITCH_STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get(_SWITCH_ENTITY_ID).state == STATE_OFF diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py index 7bcd9c09e68..9d3d2a49e26 100644 --- a/tests/components/qingping/test_config_flow.py +++ b/tests/components/qingping/test_config_flow.py @@ -114,6 +114,38 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=LIGHT_AND_SIGNAL_SERVICE_INFO.address, + source=config_entries.SOURCE_IGNORE, + data={}, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 20659182726..aefee4113cc 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -31,7 +31,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 94e80d3cd16..f09cf7493b5 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -6,19 +6,19 @@ from aioqsw.const import API_MAC_ADDR, API_PRODUCT, API_RESULT from aioqsw.exceptions import LoginError, QswError from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .util import CONFIG, LIVE_MOCK, SYSTEM_BOARD_MOCK, USERS_LOGIN_MOCK from tests.common import MockConfigEntry -DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( +DHCP_SERVICE_INFO = DhcpServiceInfo( hostname="qsw-m408-4c", ip="192.168.1.200", macaddress="245ebe000000", diff --git a/tests/components/qnap_qsw/test_sensor.py b/tests/components/qnap_qsw/test_sensor.py index 16335e878fd..1f56efded16 100644 --- a/tests/components/qnap_qsw/test_sensor.py +++ b/tests/components/qnap_qsw/test_sensor.py @@ -1,14 +1,12 @@ """The sensor tests for the QNAP QSW platform.""" -from unittest.mock import patch - from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.qnap_qsw.const import ATTR_MAX, DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.helpers import entity_registry as er from .util import async_init_integration, init_config_entry @@ -383,39 +381,6 @@ async def test_qnap_qsw_create_sensors( assert state.state == "0" -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_deprecated_uptime_seconds( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - issue_registry: ir.IssueRegistry, -) -> None: - """Test deprecation warning of the Uptime seconds sensor entity.""" - original_id = "sensor.qsw_m408_4c_uptime" - domain = Platform.SENSOR - - config_entry = init_config_entry(hass) - - entity = entity_registry.async_get_or_create( - domain=domain, - platform=DOMAIN, - unique_id=original_id, - config_entry=config_entry, - suggested_object_id=original_id, - disabled_by=None, - ) - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - - with patch( - "homeassistant.components.qnap_qsw.sensor.automations_with_entity", - return_value=["item"], - ): - await async_init_integration(hass, config_entry=config_entry) - assert issue_registry.async_get_issue( - DOMAIN, f"uptime_seconds_deprecated_{entity.entity_id}_item" - ) - - async def test_cleanup_deprecated_uptime_seconds( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index 7f9479339a5..db4f4de6c49 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -10,12 +10,12 @@ import pytest from rabbitair import Mode, Model, Speed from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.rabbitair.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo TEST_HOST = "1.1.1.1" TEST_NAME = "abcdef1234_123456789012345678" @@ -26,7 +26,7 @@ TEST_HARDWARE = "1.0.0.4" TEST_UNIQUE_ID = format_mac(TEST_MAC) TEST_TITLE = "Rabbit Air" -ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( +ZEROCONF_DATA = ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], port=9009, diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 586b31b092f..6448d46a8a1 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -4,7 +4,6 @@ from ipaddress import ip_address from unittest.mock import MagicMock, patch from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, CONF_MANUAL_RUN_MINS, @@ -13,6 +12,10 @@ from homeassistant.components.rachio.const import ( from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from tests.common import MockConfigEntry @@ -120,13 +123,13 @@ async def test_form_homekit(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + properties={ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, type="mock_type", ), ) @@ -145,13 +148,13 @@ async def test_form_homekit(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + properties={ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, type="mock_type", ), ) @@ -171,13 +174,13 @@ async def test_form_homekit_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + properties={ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, type="mock_type", ), ) diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index 563ac504057..9139e13a957 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import setup_integration diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index a188f8fcb70..a84f3870357 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -6,11 +6,11 @@ from radiotherm import CommonThermostat from radiotherm.validate import RadiothermTstatError from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.radiotherm.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -112,7 +112,7 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="radiotherm", ip="1.2.3.4", macaddress="aabbccddeeff", @@ -156,7 +156,7 @@ async def test_dhcp_fails_to_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="radiotherm", ip="1.2.3.4", macaddress="aabbccddeeff", @@ -185,7 +185,7 @@ async def test_dhcp_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="radiotherm", ip="1.2.3.4", macaddress="aabbccddeeff", diff --git a/tests/components/rainforest_raven/const.py b/tests/components/rainforest_raven/const.py index 7e75440c30d..320299d2e60 100644 --- a/tests/components/rainforest_raven/const.py +++ b/tests/components/rainforest_raven/const.py @@ -13,8 +13,9 @@ from aioraven.data import ( from iso4217 import Currency from homeassistant.components import usb +from homeassistant.helpers.service_info.usb import UsbServiceInfo -DISCOVERY_INFO = usb.UsbServiceInfo( +DISCOVERY_INFO = UsbServiceInfo( device="/dev/ttyACM0", pid="0x0003", vid="0x04B4", diff --git a/tests/components/rainforest_raven/snapshots/test_init.ambr b/tests/components/rainforest_raven/snapshots/test_init.ambr index 768bbc729d4..8a143f9963f 100644 --- a/tests/components/rainforest_raven/snapshots/test_init.ambr +++ b/tests/components/rainforest_raven/snapshots/test_init.ambr @@ -8,6 +8,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/rainforest_raven/snapshots/test_sensor.ambr b/tests/components/rainforest_raven/snapshots/test_sensor.ambr index 34a5e031885..618766c1613 100644 --- a/tests/components/rainforest_raven/snapshots/test_sensor.ambr +++ b/tests/components/rainforest_raven/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -111,6 +113,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -162,6 +165,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -213,6 +217,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/rainmachine/snapshots/test_binary_sensor.ambr b/tests/components/rainmachine/snapshots/test_binary_sensor.ambr index 9c930736fe3..c4d6f2eeae1 100644 --- a/tests/components/rainmachine/snapshots/test_binary_sensor.ambr +++ b/tests/components/rainmachine/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/rainmachine/snapshots/test_button.ambr b/tests/components/rainmachine/snapshots/test_button.ambr index 609079bb0d8..68f83d9286a 100644 --- a/tests/components/rainmachine/snapshots/test_button.ambr +++ b/tests/components/rainmachine/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/rainmachine/snapshots/test_select.ambr b/tests/components/rainmachine/snapshots/test_select.ambr index 651a709d2fa..d150f8c31b5 100644 --- a/tests/components/rainmachine/snapshots/test_select.ambr +++ b/tests/components/rainmachine/snapshots/test_select.ambr @@ -13,6 +13,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/rainmachine/snapshots/test_sensor.ambr b/tests/components/rainmachine/snapshots/test_sensor.ambr index e93d0645030..2475abecb51 100644 --- a/tests/components/rainmachine/snapshots/test_sensor.ambr +++ b/tests/components/rainmachine/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -242,6 +247,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -289,6 +295,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -336,6 +343,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -383,6 +391,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -430,6 +439,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -477,6 +487,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -524,6 +535,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -571,6 +583,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -618,6 +631,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -665,6 +679,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/rainmachine/snapshots/test_switch.ambr b/tests/components/rainmachine/snapshots/test_switch.ambr index b803ff994d4..d40913a7eb0 100644 --- a/tests/components/rainmachine/snapshots/test_switch.ambr +++ b/tests/components/rainmachine/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -78,6 +79,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -126,6 +128,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -173,6 +176,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -234,6 +238,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -282,6 +287,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -329,6 +335,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -390,6 +397,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -438,6 +446,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -510,6 +519,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -558,6 +568,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -619,6 +630,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -667,6 +679,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -728,6 +741,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -776,6 +790,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -837,6 +852,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -885,6 +901,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -946,6 +963,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -994,6 +1012,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1055,6 +1074,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1103,6 +1123,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1164,6 +1185,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1212,6 +1234,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1273,6 +1296,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1321,6 +1345,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1382,6 +1407,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1430,6 +1456,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1491,6 +1518,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1539,6 +1567,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1600,6 +1629,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 5838dcc35c8..cd8b2cb39c8 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -7,7 +7,6 @@ import pytest from regenmaschine.errors import RainMachineError from homeassistant import config_entries, setup -from homeassistant.components import zeroconf from homeassistant.components.rainmachine import ( CONF_ALLOW_INACTIVE_ZONES_TO_RUN, CONF_DEFAULT_ZONE_RUN_TIME, @@ -18,6 +17,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo async def test_duplicate_error(hass: HomeAssistant, config, config_entry) -> None: @@ -168,7 +168,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.100"), ip_addresses=[ip_address("192.168.1.100")], hostname="mock_hostname", @@ -196,7 +196,7 @@ async def test_step_homekit_zeroconf_ip_change( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.2"), ip_addresses=[ip_address("192.168.1.2")], hostname="mock_hostname", @@ -225,7 +225,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.100"), ip_addresses=[ip_address("192.168.1.100")], hostname="mock_hostname", @@ -279,7 +279,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.100"), ip_addresses=[ip_address("192.168.1.100")], hostname="mock_hostname", @@ -299,7 +299,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time( result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.100"), ip_addresses=[ip_address("192.168.1.100")], hostname="mock_hostname", diff --git a/tests/components/recorder/auto_repairs/events/test_schema.py b/tests/components/recorder/auto_repairs/events/test_schema.py index cae181a6270..91f5bd50298 100644 --- a/tests/components/recorder/auto_repairs/events/test_schema.py +++ b/tests/components/recorder/auto_repairs/events/test_schema.py @@ -8,12 +8,12 @@ from homeassistant.core import HomeAssistant from ...common import async_wait_recording_done -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -22,7 +22,7 @@ async def mock_recorder_before_hass( @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -58,7 +58,7 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_event_data( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -91,7 +91,7 @@ async def test_validate_db_schema_fix_utf8_issue_event_data( @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, diff --git a/tests/components/recorder/auto_repairs/states/test_schema.py b/tests/components/recorder/auto_repairs/states/test_schema.py index 915ac1f3500..982a6a732b6 100644 --- a/tests/components/recorder/auto_repairs/states/test_schema.py +++ b/tests/components/recorder/auto_repairs/states/test_schema.py @@ -8,12 +8,12 @@ from homeassistant.core import HomeAssistant from ...common import async_wait_recording_done -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -22,7 +22,7 @@ async def mock_recorder_before_hass( @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -60,7 +60,7 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_states( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -92,7 +92,7 @@ async def test_validate_db_schema_fix_utf8_issue_states( @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_utf8_issue_state_attributes( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -125,7 +125,7 @@ async def test_validate_db_schema_fix_utf8_issue_state_attributes( @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index 9e287d13594..2466a761364 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -17,17 +17,17 @@ from homeassistant.components.recorder.auto_repairs.statistics.duplicates import from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from ...common import async_wait_recording_done from tests.common import async_test_home_assistant -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -134,7 +134,7 @@ def _create_engine_28(*args, **kwargs): @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_delete_metadata_duplicates( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, ) -> None: """Test removal of duplicated statistics.""" @@ -242,7 +242,7 @@ async def test_delete_metadata_duplicates( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_delete_metadata_duplicates_many( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, ) -> None: """Test removal of duplicated statistics.""" diff --git a/tests/components/recorder/auto_repairs/statistics/test_schema.py b/tests/components/recorder/auto_repairs/statistics/test_schema.py index 34a075afbc7..352a2345052 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_schema.py +++ b/tests/components/recorder/auto_repairs/statistics/test_schema.py @@ -8,12 +8,12 @@ from homeassistant.core import HomeAssistant from ...common import async_wait_recording_done -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -22,7 +22,7 @@ async def mock_recorder_before_hass( @pytest.mark.parametrize("enable_schema_validation", [True]) async def test_validate_db_schema_fix_utf8_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, db_engine: str, recorder_dialect_name: None, @@ -56,7 +56,7 @@ async def test_validate_db_schema_fix_utf8_issue( @pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, table: str, db_engine: str, @@ -100,7 +100,7 @@ async def test_validate_db_schema_fix_float_issue( @pytest.mark.parametrize("db_engine", ["mysql"]) async def test_validate_db_schema_fix_collation_issue( hass: HomeAssistant, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, recorder_dialect_name: None, db_engine: str, diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index 857c0f6572f..bf2a925df17 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -18,12 +18,12 @@ from homeassistant.core import HomeAssistant from ..common import async_wait_recording_done -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index fbb0991c960..792000c3725 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -37,7 +37,7 @@ from homeassistant.components.recorder.db_schema import ( from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask from homeassistant.const import UnitOfTemperature from homeassistant.core import Event, HomeAssistant, State -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import db_schema_0 diff --git a/tests/components/recorder/db_schema_0.py b/tests/components/recorder/db_schema_0.py index 12336dcc96a..12228e99211 100644 --- a/tests/components/recorder/db_schema_0.py +++ b/tests/components/recorder/db_schema_0.py @@ -23,7 +23,7 @@ from sqlalchemy.orm.session import Session from homeassistant.core import Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index d7ca35c9341..3455af1d019 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -347,11 +347,11 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", + "_row", ] def __init__(self, row) -> None: # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_18.py b/tests/components/recorder/db_schema_18.py index adb71dffb9e..9e9dc786580 100644 --- a/tests/components/recorder/db_schema_18.py +++ b/tests/components/recorder/db_schema_18.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -360,11 +360,11 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", + "_row", ] def __init__(self, row) -> None: # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_22.py b/tests/components/recorder/db_schema_22.py index c0d607b12a7..766ff88ff72 100644 --- a/tests/components/recorder/db_schema_22.py +++ b/tests/components/recorder/db_schema_22.py @@ -42,7 +42,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -479,11 +479,11 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", + "_row", ] def __init__(self, row) -> None: # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_23.py b/tests/components/recorder/db_schema_23.py index f60b7b49df4..fe36029b61f 100644 --- a/tests/components/recorder/db_schema_23.py +++ b/tests/components/recorder/db_schema_23.py @@ -41,7 +41,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -469,11 +469,11 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", + "_row", ] def __init__(self, row) -> None: # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index 4cc1074de41..a77bc1fcbd5 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -49,7 +49,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -593,11 +593,11 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", + "_row", ] def __init__(self, row) -> None: # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py index d989cacb76a..bd3cb23bd07 100644 --- a/tests/components/recorder/db_schema_25.py +++ b/tests/components/recorder/db_schema_25.py @@ -37,7 +37,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.typing import UNDEFINED, UndefinedType -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -529,12 +529,12 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", + "_attr_cache", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", - "_attr_cache", + "_row", ] def __init__( # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_28.py b/tests/components/recorder/db_schema_28.py index 8c984b61f6c..7f34343d995 100644 --- a/tests/components/recorder/db_schema_28.py +++ b/tests/components/recorder/db_schema_28.py @@ -43,7 +43,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() @@ -694,12 +694,12 @@ class LazyState(State): """A lazy version of core State.""" __slots__ = [ - "_row", + "_attr_cache", "_attributes", + "_context", "_last_changed", "_last_updated", - "_context", - "_attr_cache", + "_row", ] def __init__( # pylint: disable=super-init-not-called diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py index 97c33334111..185dce786de 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -50,7 +50,7 @@ from homeassistant.const import ( from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers import entity_registry as er from homeassistant.helpers.json import JSON_DUMP, json_bytes -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} diff --git a/tests/components/recorder/db_schema_32.py b/tests/components/recorder/db_schema_32.py index 39ddb8e3148..daa7fb6977c 100644 --- a/tests/components/recorder/db_schema_32.py +++ b/tests/components/recorder/db_schema_32.py @@ -51,7 +51,7 @@ from homeassistant.const import ( from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers import entity_registry as er from homeassistant.helpers.json import JSON_DUMP, json_bytes -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py index efeade46562..a5381d633cb 100644 --- a/tests/components/recorder/db_schema_42.py +++ b/tests/components/recorder/db_schema_42.py @@ -66,7 +66,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, State from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.json import ( JSON_DECODE_EXCEPTIONS, json_loads, diff --git a/tests/components/recorder/db_schema_43.py b/tests/components/recorder/db_schema_43.py index 8e77e8782ee..379e6fbd416 100644 --- a/tests/components/recorder/db_schema_43.py +++ b/tests/components/recorder/db_schema_43.py @@ -66,7 +66,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, Event, EventOrigin, EventStateChangedData, State from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.json import ( JSON_DECODE_EXCEPTIONS, json_loads, diff --git a/tests/components/recorder/db_schema_9.py b/tests/components/recorder/db_schema_9.py index f9a8c2d2cad..6cf7085e279 100644 --- a/tests/components/recorder/db_schema_9.py +++ b/tests/components/recorder/db_schema_9.py @@ -19,13 +19,12 @@ from sqlalchemy import ( Text, distinct, ) -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util # SQLAlchemy Schema Base = declarative_base() diff --git a/tests/components/recorder/table_managers/test_recorder_runs.py b/tests/components/recorder/table_managers/test_recorder_runs.py index 41f3a8fef4d..e79def01bad 100644 --- a/tests/components/recorder/table_managers/test_recorder_runs.py +++ b/tests/components/recorder/table_managers/test_recorder_runs.py @@ -21,6 +21,11 @@ async def test_run_history(recorder_mock: Recorder, hass: HomeAssistant) -> None two_days_ago = now - timedelta(days=2) one_day_ago = now - timedelta(days=1) + # Test that the first run falls back to the current run + assert process_timestamp( + instance.recorder_runs_manager.first.start + ) == process_timestamp(instance.recorder_runs_manager.current.start) + with instance.get_session() as session: session.add(RecorderRuns(start=three_days_ago, created=three_days_ago)) session.add(RecorderRuns(start=two_days_ago, created=two_days_ago)) @@ -29,32 +34,7 @@ async def test_run_history(recorder_mock: Recorder, hass: HomeAssistant) -> None instance.recorder_runs_manager.load_from_db(session) assert ( - process_timestamp( - instance.recorder_runs_manager.get( - three_days_ago + timedelta(microseconds=1) - ).start - ) - == three_days_ago - ) - assert ( - process_timestamp( - instance.recorder_runs_manager.get( - two_days_ago + timedelta(microseconds=1) - ).start - ) - == two_days_ago - ) - assert ( - process_timestamp( - instance.recorder_runs_manager.get( - one_day_ago + timedelta(microseconds=1) - ).start - ) - == one_day_ago - ) - assert ( - process_timestamp(instance.recorder_runs_manager.get(now).start) - == instance.recorder_runs_manager.recording_start + process_timestamp(instance.recorder_runs_manager.first.start) == three_days_ago ) diff --git a/tests/components/recorder/test_entity_registry.py b/tests/components/recorder/test_entity_registry.py index ad438dcc525..8a5ce23799c 100644 --- a/tests/components/recorder/test_entity_registry.py +++ b/tests/components/recorder/test_entity_registry.py @@ -23,7 +23,7 @@ from .common import ( ) from tests.common import MockEntity, MockEntityPlatform -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager def _count_entity_id_in_states_meta( @@ -40,7 +40,7 @@ def _count_entity_id_in_states_meta( @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 28b8275247c..166451cc971 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -22,7 +22,7 @@ from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( assert_dict_of_states_equal_without_context_and_last_changed, @@ -33,12 +33,12 @@ from .common import ( async_wait_recording_done, ) -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index 666626ff688..142d2fc87f6 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -17,7 +17,7 @@ from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( assert_dict_of_states_equal_without_context_and_last_changed, @@ -28,12 +28,12 @@ from .common import ( old_db_schema, ) -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py index 85badeea281..1523f373ea8 100644 --- a/tests/components/recorder/test_history_db_schema_42.py +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -10,15 +10,15 @@ from unittest.mock import sentinel from freezegun import freeze_time import pytest +from homeassistant import core as ha from homeassistant.components import recorder from homeassistant.components.recorder import Recorder, history from homeassistant.components.recorder.filters import Filters from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.util import session_scope -import homeassistant.core as ha from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( assert_dict_of_states_equal_without_context_and_last_changed, @@ -31,12 +31,12 @@ from .common import ( ) from .db_schema_42 import StateAttributes, States, StatesMeta -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index d16712e0c70..f8d1ac4af57 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -9,7 +9,7 @@ import sqlite3 import sys import threading from typing import Any, cast -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -98,12 +98,12 @@ from tests.common import ( async_test_home_assistant, mock_platform, ) -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager, RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -964,12 +964,17 @@ async def test_recorder_setup_failure(hass: HomeAssistant) -> None: hass.stop() -async def test_recorder_validate_schema_failure(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "function_to_patch", ["_get_current_schema_version", "_get_initial_schema_version"] +) +async def test_recorder_validate_schema_failure( + hass: HomeAssistant, function_to_patch: str +) -> None: """Test some exceptions.""" recorder_helper.async_initialize_recorder(hass) with ( patch( - "homeassistant.components.recorder.migration._get_schema_version" + f"homeassistant.components.recorder.migration.{function_to_patch}" ) as inspect_schema_version, patch("homeassistant.components.recorder.core.time.sleep"), ): @@ -1368,7 +1373,7 @@ async def test_statistics_runs_initiated( @pytest.mark.parametrize("enable_missing_statistics", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_compile_missing_statistics( - async_test_recorder: RecorderInstanceGenerator, freezer: FrozenDateTimeFactory + async_test_recorder: RecorderInstanceContextManager, freezer: FrozenDateTimeFactory ) -> None: """Test missing statistics are compiled on startup.""" now = dt_util.utcnow().replace(minute=0, second=0, microsecond=0) @@ -1627,7 +1632,7 @@ async def test_service_disable_states_not_recording( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_service_disable_run_information_recorded( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test that runs are still recorded when recorder is disabled.""" @@ -2570,23 +2575,25 @@ async def test_clean_shutdown_when_recorder_thread_raises_during_validate_db_sch @pytest.mark.parametrize( ("func_to_patch", "expected_setup_result"), - [("migrate_schema_non_live", False), ("migrate_schema_live", False)], + [ + ("migrate_schema_non_live", False), + ("migrate_schema_live", True), + ], ) async def test_clean_shutdown_when_schema_migration_fails( - hass: HomeAssistant, func_to_patch: str, expected_setup_result: bool + hass: HomeAssistant, + func_to_patch: str, + expected_setup_result: bool, + caplog: pytest.LogCaptureFixture, ) -> None: """Test we still shutdown cleanly when schema migration fails.""" with ( - patch.object( - migration, - "validate_db_schema", - return_value=MagicMock(valid=False, current_version=1), - ), + patch.object(migration, "_get_current_schema_version", side_effect=[None, 1]), patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch.object( migration, func_to_patch, - side_effect=Exception, + side_effect=Exception("Boom!"), ), ): if recorder.DOMAIN not in hass.data: @@ -2605,9 +2612,53 @@ async def test_clean_shutdown_when_schema_migration_fails( assert setup_result == expected_setup_result await hass.async_block_till_done() + instance = recorder.get_instance(hass) + await hass.async_stop() + assert instance.engine is None + + assert "Error during schema migration" in caplog.text + # Check the injected exception was logged + assert "Boom!" in caplog.text + + +async def test_setup_fails_after_downgrade( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we fail to setup after a downgrade. + + Also test we shutdown cleanly. + """ + with ( + patch.object( + migration, + "_get_current_schema_version", + side_effect=[None, SCHEMA_VERSION + 1], + ), + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) + assert not await async_setup_component( + hass, + recorder.DOMAIN, + { + recorder.DOMAIN: { + CONF_DB_URL: "sqlite://", + CONF_DB_RETRY_WAIT: 0, + CONF_DB_MAX_RETRIES: 1, + } + }, + ) + await hass.async_block_till_done() + instance = recorder.get_instance(hass) await hass.async_stop() assert instance.engine is None + assert ( + f"The database schema version {SCHEMA_VERSION + 1} is newer " + f"than {SCHEMA_VERSION} which is the maximum database schema " + "version supported by the installed version of Home Assistant Core" + ) in caplog.text async def test_events_are_recorded_until_final_write( diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 14978bee5a9..081394c780c 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -31,18 +31,18 @@ from homeassistant.components.recorder.db_schema import ( from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant, State from homeassistant.helpers import recorder as recorder_helper -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import async_wait_recording_done, create_engine_test from .conftest import InstrumentedMigration from tests.common import async_fire_time_changed -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager, RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -97,6 +97,7 @@ async def test_schema_update_calls( session_maker, migration.SchemaValidationStatus( current_version=0, + initial_version=0, migration_needed=True, non_live_data_migration_needed=True, schema_errors=set(), @@ -111,6 +112,7 @@ async def test_schema_update_calls( session_maker, migration.SchemaValidationStatus( current_version=42, + initial_version=0, migration_needed=True, non_live_data_migration_needed=True, schema_errors=set(), @@ -598,7 +600,7 @@ async def test_schema_migrate( start=self.recorder_runs_manager.recording_start, created=dt_util.utcnow() ) - def _sometimes_failing_create_index(*args): + def _sometimes_failing_create_index(*args, **kwargs): """Make the first index create raise a retryable error to ensure we retry.""" if recorder_db_url.startswith("mysql://"): nonlocal create_calls @@ -607,7 +609,7 @@ async def test_schema_migrate( mysql_exception = OperationalError("statement", {}, []) mysql_exception.orig = Exception(1205, "retryable") raise mysql_exception - real_create_index(*args) + real_create_index(*args, **kwargs) with ( patch( @@ -710,7 +712,7 @@ def test_forgiving_add_index(recorder_db_url: str) -> None: instance = Mock() instance.get_session = Mock(return_value=session) migration._create_index( - instance.get_session, "states", "ix_states_context_id_bin" + instance, instance.get_session, "states", "ix_states_context_id_bin" ) engine.dispose() @@ -786,7 +788,7 @@ def test_forgiving_add_index_with_other_db_types( with patch( "homeassistant.components.recorder.migration.Table", return_value=mocked_table ): - migration._create_index(Mock(), "states", "ix_states_context_id") + migration._create_index(Mock(), Mock(), "states", "ix_states_context_id") assert "already exists on states" in caplog.text assert "continuing" in caplog.text diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index e42cd22e952..0a5f5d4da73 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -13,6 +13,7 @@ import pytest from sqlalchemy import create_engine, inspect from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session +from sqlalchemy.schema import Index from homeassistant.components import recorder from homeassistant.components.recorder import ( @@ -40,7 +41,7 @@ from homeassistant.components.recorder.util import ( session_scope, ) from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.ulid import bytes_to_ulid, ulid_at_time, ulid_to_bytes from .common import ( @@ -51,7 +52,7 @@ from .common import ( from .conftest import instrument_migration from tests.common import async_test_home_assistant -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" SCHEMA_MODULE_32 = "tests.components.recorder.db_schema_32" @@ -59,7 +60,7 @@ SCHEMA_MODULE_32 = "tests.components.recorder.db_schema_32" @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -120,9 +121,11 @@ def db_schema_32(): @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.parametrize("enable_migrate_event_context_ids", [True]) +@pytest.mark.parametrize("indices_to_drop", [[], [("events", "ix_events_context_id")]]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_events_context_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, + indices_to_drop: list[tuple[str, str]], ) -> None: """Test we can migrate old uuid context ids and ulid context ids to binary format.""" importlib.import_module(SCHEMA_MODULE_32) @@ -237,6 +240,13 @@ async def test_migrate_events_context_ids( ] await _async_wait_migration_done(hass) + # Remove index + instance.recorder_and_worker_thread_ids.add(threading.get_ident()) + for table, index in indices_to_drop: + with session_scope(hass=hass) as session: + assert get_index_by_name(session, table, index) is not None + migration._drop_index(instance.get_session, table, index) + await hass.async_stop() await hass.async_block_till_done() @@ -266,7 +276,13 @@ async def test_migrate_events_context_ids( # Run again with new schema, let migration run async with async_test_home_assistant() as hass: - with freeze_time(now), instrument_migration(hass) as instrumented_migration: + with ( + freeze_time(now), + instrument_migration(hass) as instrumented_migration, + patch( + "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create + ) as wrapped_idx_create, + ): async with async_test_recorder( hass, wait_recorder=False, wait_recorder_setup=False ) as instance: @@ -297,6 +313,10 @@ async def test_migrate_events_context_ids( await hass.async_stop() await hass.async_block_till_done() + # Check the index we removed was recreated + index_names = [call[1][0].name for call in wrapped_idx_create.mock_calls] + assert index_names == [index for _, index in indices_to_drop] + old_uuid_context_id_event = events_by_type["old_uuid_context_id_event"] assert old_uuid_context_id_event["context_id"] is None assert old_uuid_context_id_event["context_user_id"] is None @@ -376,7 +396,7 @@ async def test_migrate_events_context_ids( @pytest.mark.parametrize("enable_migrate_event_context_ids", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_finish_migrate_events_context_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test we re migrate old uuid context ids and ulid context ids to binary format. @@ -482,9 +502,11 @@ async def test_finish_migrate_events_context_ids( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.parametrize("enable_migrate_state_context_ids", [True]) +@pytest.mark.parametrize("indices_to_drop", [[], [("states", "ix_states_context_id")]]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_states_context_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, + indices_to_drop: list[tuple[str, str]], ) -> None: """Test we can migrate old uuid context ids and ulid context ids to binary format.""" importlib.import_module(SCHEMA_MODULE_32) @@ -577,6 +599,13 @@ async def test_migrate_states_context_ids( await async_wait_recording_done(hass) await _async_wait_migration_done(hass) + # Remove index + instance.recorder_and_worker_thread_ids.add(threading.get_ident()) + for table, index in indices_to_drop: + with session_scope(hass=hass) as session: + assert get_index_by_name(session, table, index) is not None + migration._drop_index(instance.get_session, table, index) + await hass.async_stop() await hass.async_block_till_done() @@ -606,7 +635,12 @@ async def test_migrate_states_context_ids( # Run again with new schema, let migration run async with async_test_home_assistant() as hass: - with instrument_migration(hass) as instrumented_migration: + with ( + instrument_migration(hass) as instrumented_migration, + patch( + "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create + ) as wrapped_idx_create, + ): async with async_test_recorder( hass, wait_recorder=False, wait_recorder_setup=False ) as instance: @@ -637,6 +671,10 @@ async def test_migrate_states_context_ids( await hass.async_stop() await hass.async_block_till_done() + # Check the index we removed was recreated + index_names = [call[1][0].name for call in wrapped_idx_create.mock_calls] + assert index_names == [index for _, index in indices_to_drop] + old_uuid_context_id = states_by_entity_id["state.old_uuid_context_id"] assert old_uuid_context_id["context_id"] is None assert old_uuid_context_id["context_user_id"] is None @@ -720,7 +758,7 @@ async def test_migrate_states_context_ids( @pytest.mark.parametrize("enable_migrate_state_context_ids", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_finish_migrate_states_context_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test we re migrate old uuid context ids and ulid context ids to binary format. @@ -828,7 +866,7 @@ async def test_finish_migrate_states_context_ids( @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_event_type_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test we can migrate event_types to the EventTypes table.""" importlib.import_module(SCHEMA_MODULE_32) @@ -946,7 +984,7 @@ async def test_migrate_event_type_ids( @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_entity_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" importlib.import_module(SCHEMA_MODULE_32) @@ -1049,9 +1087,13 @@ async def test_migrate_entity_ids( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) +@pytest.mark.parametrize( + "indices_to_drop", [[], [("states", "ix_states_entity_id_last_updated_ts")]] +) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_post_migrate_entity_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, + indices_to_drop: list[tuple[str, str]], ) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" importlib.import_module(SCHEMA_MODULE_32) @@ -1096,6 +1138,13 @@ async def test_post_migrate_entity_ids( await async_wait_recording_done(hass) await _async_wait_migration_done(hass) + # Remove index + instance.recorder_and_worker_thread_ids.add(threading.get_ident()) + for table, index in indices_to_drop: + with session_scope(hass=hass) as session: + assert get_index_by_name(session, table, index) is not None + migration._drop_index(instance.get_session, table, index) + await hass.async_stop() await hass.async_block_till_done() @@ -1109,20 +1158,38 @@ async def test_post_migrate_entity_ids( return {state.state: state.entity_id for state in states} # Run again with new schema, let migration run - async with ( - async_test_home_assistant() as hass, - async_test_recorder(hass) as instance, - ): - instance.recorder_and_worker_thread_ids.add(threading.get_ident()) + with patch( + "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create + ) as wrapped_idx_create: + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + instance.recorder_and_worker_thread_ids.add(threading.get_ident()) - await hass.async_block_till_done() - await async_wait_recording_done(hass) - await async_wait_recording_done(hass) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await async_wait_recording_done(hass) - states_by_state = await instance.async_add_executor_job(_fetch_migrated_states) + states_by_state = await instance.async_add_executor_job( + _fetch_migrated_states + ) - await hass.async_stop() - await hass.async_block_till_done() + # Check the index which will be removed by the migrator no longer exists + with session_scope(hass=hass) as session: + assert ( + get_index_by_name( + session, "states", "ix_states_entity_id_last_updated_ts" + ) + is None + ) + + await hass.async_stop() + await hass.async_block_till_done() + + # Check the index we removed was recreated + index_names = [call[1][0].name for call in wrapped_idx_create.mock_calls] + assert index_names == [index for _, index in indices_to_drop] assert states_by_state["one_1"] is None assert states_by_state["two_2"] is None @@ -1133,7 +1200,7 @@ async def test_post_migrate_entity_ids( @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_null_entity_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test we can migrate entity_ids to the StatesMeta table.""" importlib.import_module(SCHEMA_MODULE_32) @@ -1243,7 +1310,7 @@ async def test_migrate_null_entity_ids( @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_null_event_type_ids( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test we can migrate event_types to the EventTypes table when the event_type is NULL.""" importlib.import_module(SCHEMA_MODULE_32) @@ -1924,7 +1991,7 @@ async def test_stats_timestamp_with_one_by_one_removes_duplicates( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_stats_migrate_times( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, ) -> None: """Test we can migrate times in the statistics tables.""" @@ -2075,3 +2142,143 @@ async def test_stats_migrate_times( ) await hass.async_stop() + + +@pytest.mark.parametrize("persistent_database", [True]) +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +async def test_cleanup_unmigrated_state_timestamps( + async_test_recorder: RecorderInstanceContextManager, +) -> None: + """Ensure schema 48 migration cleans up any unmigrated state timestamps.""" + importlib.import_module(SCHEMA_MODULE_32) + old_db_schema = sys.modules[SCHEMA_MODULE_32] + + test_uuid = uuid.uuid4() + uuid_hex = test_uuid.hex + + def _object_as_dict(obj): + return {c.key: getattr(obj, c.key) for c in inspect(obj).mapper.column_attrs} + + def _insert_states(): + with session_scope(hass=hass) as session: + state1 = old_db_schema.States( + entity_id="state.test_state1", + last_updated=datetime.datetime( + 2016, 10, 28, 20, 13, 52, 452529, tzinfo=datetime.UTC + ), + last_updated_ts=None, + last_changed=datetime.datetime( + 2016, 10, 28, 20, 13, 52, 452529, tzinfo=datetime.UTC + ), + last_changed_ts=None, + context_id=uuid_hex, + context_id_bin=None, + context_user_id=None, + context_user_id_bin=None, + context_parent_id=None, + context_parent_id_bin=None, + ) + state2 = old_db_schema.States( + entity_id="state.test_state2", + last_updated=datetime.datetime( + 2016, 10, 28, 20, 13, 52, 552529, tzinfo=datetime.UTC + ), + last_updated_ts=None, + last_changed=datetime.datetime( + 2016, 10, 28, 20, 13, 52, 452529, tzinfo=datetime.UTC + ), + last_changed_ts=None, + context_id=None, + context_id_bin=None, + context_user_id=None, + context_user_id_bin=None, + context_parent_id=None, + context_parent_id_bin=None, + ) + session.add_all((state1, state2)) + # There is a default of now() for last_updated_ts so make sure it's not set + session.query(old_db_schema.States).update( + {old_db_schema.States.last_updated_ts: None} + ) + state3 = old_db_schema.States( + entity_id="state.already_migrated", + last_updated=None, + last_updated_ts=1477685632.452529, + last_changed=None, + last_changed_ts=1477685632.452529, + context_id=uuid_hex, + context_id_bin=None, + context_user_id=None, + context_user_id_bin=None, + context_parent_id=None, + context_parent_id_bin=None, + ) + session.add_all((state3,)) + + with session_scope(hass=hass, read_only=True) as session: + states = session.query(old_db_schema.States).all() + assert len(states) == 3 + + # Create database with old schema + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object(migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass) as instance, + ): + await instance.async_add_executor_job(_insert_states) + + await async_wait_recording_done(hass) + now = dt_util.utcnow() + await _async_wait_migration_done(hass) + await async_wait_recording_done(hass) + + await hass.async_stop() + await hass.async_block_till_done() + + def _fetch_migrated_states(): + with session_scope(hass=hass) as session: + states = session.query(States).all() + assert len(states) == 3 + return {state.state_id: _object_as_dict(state) for state in states} + + # Run again with new schema, let migration run + async with async_test_home_assistant() as hass: + with ( + freeze_time(now), + instrument_migration(hass) as instrumented_migration, + ): + async with async_test_recorder( + hass, wait_recorder=False, wait_recorder_setup=False + ) as instance: + # Check the context ID migrator is considered non-live + assert recorder.util.async_migration_is_live(hass) is False + instrumented_migration.migration_stall.set() + instance.recorder_and_worker_thread_ids.add(threading.get_ident()) + + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await async_wait_recording_done(hass) + + states_by_metadata_id = await instance.async_add_executor_job( + _fetch_migrated_states + ) + + await hass.async_stop() + await hass.async_block_till_done() + + assert len(states_by_metadata_id) == 3 + for state in states_by_metadata_id.values(): + assert state["last_updated_ts"] is not None + + by_entity_id = { + state["entity_id"]: state for state in states_by_metadata_id.values() + } + assert by_entity_id["state.test_state1"]["last_updated_ts"] == 1477685632.452529 + assert by_entity_id["state.test_state2"]["last_updated_ts"] == 1477685632.552529 + assert ( + by_entity_id["state.already_migrated"]["last_updated_ts"] == 1477685632.452529 + ) diff --git a/tests/components/recorder/test_migration_run_time_migrations_remember.py b/tests/components/recorder/test_migration_run_time_migrations_remember.py index 7a333b0a2f5..43a1b028348 100644 --- a/tests/components/recorder/test_migration_run_time_migrations_remember.py +++ b/tests/components/recorder/test_migration_run_time_migrations_remember.py @@ -1,15 +1,18 @@ """Test run time migrations are remembered in the migration_changes table.""" +from collections.abc import Callable, Generator import importlib import sys -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session +from sqlalchemy.schema import Index from homeassistant.components import recorder from homeassistant.components.recorder import core, migration, statistics +from homeassistant.components.recorder.db_schema import SCHEMA_VERSION from homeassistant.components.recorder.migration import MigrationTask from homeassistant.components.recorder.queries import get_migration_changes from homeassistant.components.recorder.util import ( @@ -22,15 +25,16 @@ from homeassistant.core import HomeAssistant from .common import async_recorder_block_till_done, async_wait_recording_done from tests.common import async_test_home_assistant -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" -SCHEMA_MODULE = "tests.components.recorder.db_schema_32" +SCHEMA_MODULE_32 = "tests.components.recorder.db_schema_32" +SCHEMA_MODULE_CURRENT = "homeassistant.components.recorder.db_schema" @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -46,33 +50,231 @@ def _get_migration_id(hass: HomeAssistant) -> dict[str, int]: return dict(execute_stmt_lambda_element(session, get_migration_changes())) -def _create_engine_test(*args, **kwargs): +def _create_engine_test( + schema_module: str, *, initial_version: int | None = None +) -> Callable: """Test version of create_engine that initializes with old schema. This simulates an existing db with the old schema. """ - importlib.import_module(SCHEMA_MODULE) - old_db_schema = sys.modules[SCHEMA_MODULE] - engine = create_engine(*args, **kwargs) - old_db_schema.Base.metadata.create_all(engine) - with Session(engine) as session: - session.add( - recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) - ) - session.add( - recorder.db_schema.SchemaChanges( - schema_version=old_db_schema.SCHEMA_VERSION + + def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + importlib.import_module(schema_module) + old_db_schema = sys.modules[schema_module] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) ) + if initial_version is not None: + session.add( + recorder.db_schema.SchemaChanges(schema_version=initial_version) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) + ) + session.commit() + return engine + + return _create_engine_test + + +@pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage +@pytest.mark.parametrize( + ("initial_version", "expected_migrator_calls", "expected_created_indices"), + # expected_migrator_calls is a dict of + # migrator_id: (needs_migrate_calls, migrate_data_calls) + [ + ( + 27, + { + "state_context_id_as_binary": (0, 1), + "event_context_id_as_binary": (0, 1), + "event_type_id_migration": (2, 1), + "entity_id_migration": (2, 1), + "event_id_post_migration": (1, 1), + "entity_id_post_migration": (0, 1), + }, + [ + "ix_states_context_id", + "ix_events_context_id", + "ix_states_entity_id_last_updated_ts", + ], + ), + ( + 28, + { + "state_context_id_as_binary": (0, 1), + "event_context_id_as_binary": (0, 1), + "event_type_id_migration": (2, 1), + "entity_id_migration": (2, 1), + "event_id_post_migration": (0, 0), + "entity_id_post_migration": (0, 1), + }, + [ + "ix_states_context_id", + "ix_events_context_id", + "ix_states_entity_id_last_updated_ts", + ], + ), + ( + 36, + { + "state_context_id_as_binary": (0, 0), + "event_context_id_as_binary": (0, 0), + "event_type_id_migration": (2, 1), + "entity_id_migration": (2, 1), + "event_id_post_migration": (0, 0), + "entity_id_post_migration": (0, 1), + }, + ["ix_states_entity_id_last_updated_ts"], + ), + ( + 37, + { + "state_context_id_as_binary": (0, 0), + "event_context_id_as_binary": (0, 0), + "event_type_id_migration": (0, 0), + "entity_id_migration": (2, 1), + "event_id_post_migration": (0, 0), + "entity_id_post_migration": (0, 1), + }, + ["ix_states_entity_id_last_updated_ts"], + ), + ( + 38, + { + "state_context_id_as_binary": (0, 0), + "event_context_id_as_binary": (0, 0), + "event_type_id_migration": (0, 0), + "entity_id_migration": (0, 0), + "event_id_post_migration": (0, 0), + "entity_id_post_migration": (0, 0), + }, + [], + ), + ( + SCHEMA_VERSION, + { + "state_context_id_as_binary": (0, 0), + "event_context_id_as_binary": (0, 0), + "event_type_id_migration": (0, 0), + "entity_id_migration": (0, 0), + "event_id_post_migration": (0, 0), + "entity_id_post_migration": (0, 0), + }, + [], + ), + ], +) +async def test_data_migrator_logic( + async_test_recorder: RecorderInstanceContextManager, + initial_version: int, + expected_migrator_calls: dict[str, tuple[int, int]], + expected_created_indices: list[str], +) -> None: + """Test the data migrator logic. + + - The data migrators should not be executed on a new database. + - Indices needed by the migrators should be created if missing. + """ + config = {recorder.CONF_COMMIT_INTERVAL: 1} + + def migrator_mock() -> dict[str, Mock]: + return { + "needs_migrate": Mock( + spec_set=[], + return_value=migration.DataMigrationStatus( + needs_migrate=True, migration_done=False + ), + ), + "migrate_data": Mock(spec_set=[], return_value=True), + } + + migrator_mocks = { + "state_context_id_as_binary": migrator_mock(), + "event_context_id_as_binary": migrator_mock(), + "event_type_id_migration": migrator_mock(), + "entity_id_migration": migrator_mock(), + "event_id_post_migration": migrator_mock(), + "entity_id_post_migration": migrator_mock(), + } + + def patch_check( + migrator_id: str, migrator_class: type[migration.BaseMigration] + ) -> Generator[None]: + return patch.object( + migrator_class, + "needs_migrate_impl", + side_effect=migrator_mocks[migrator_id]["needs_migrate"], ) - session.commit() - return engine + + def patch_migrate( + migrator_id: str, migrator_class: type[migration.BaseMigration] + ) -> Generator[None]: + return patch.object( + migrator_class, + "migrate_data", + side_effect=migrator_mocks[migrator_id]["migrate_data"], + ) + + with ( + patch_check("state_context_id_as_binary", migration.StatesContextIDMigration), + patch_check("event_context_id_as_binary", migration.EventsContextIDMigration), + patch_check("event_type_id_migration", migration.EventTypeIDMigration), + patch_check("entity_id_migration", migration.EntityIDMigration), + patch_check("event_id_post_migration", migration.EventIDPostMigration), + patch_check("entity_id_post_migration", migration.EntityIDPostMigration), + patch_migrate("state_context_id_as_binary", migration.StatesContextIDMigration), + patch_migrate("event_context_id_as_binary", migration.EventsContextIDMigration), + patch_migrate("event_type_id_migration", migration.EventTypeIDMigration), + patch_migrate("entity_id_migration", migration.EntityIDMigration), + patch_migrate("event_id_post_migration", migration.EventIDPostMigration), + patch_migrate("entity_id_post_migration", migration.EntityIDPostMigration), + patch( + CREATE_ENGINE_TARGET, + new=_create_engine_test( + SCHEMA_MODULE_CURRENT, initial_version=initial_version + ), + ), + patch( + "sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create + ) as wrapped_idx_create, + ): + async with ( + async_test_home_assistant() as hass, + async_test_recorder(hass, config), + ): + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await _async_wait_migration_done(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + await hass.async_stop() + + index_names = [call[1][0].name for call in wrapped_idx_create.mock_calls] + assert index_names == expected_created_indices + + # Check each data migrator's needs_migrate_impl and migrate_data methods were called + # the expected number of times. + for migrator, mock in migrator_mocks.items(): + needs_migrate_calls, migrate_data_calls = expected_migrator_calls[migrator] + assert len(mock["needs_migrate"].mock_calls) == needs_migrate_calls + assert len(mock["migrate_data"].mock_calls) == migrate_data_calls @pytest.mark.parametrize("enable_migrate_state_context_ids", [True]) @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migration_changes_prevent_trying_to_migrate_again( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Test that we do not try to migrate when migration_changes indicate its already migrated. @@ -84,8 +286,8 @@ async def test_migration_changes_prevent_trying_to_migrate_again( """ config = {recorder.CONF_COMMIT_INTERVAL: 1} - importlib.import_module(SCHEMA_MODULE) - old_db_schema = sys.modules[SCHEMA_MODULE] + importlib.import_module(SCHEMA_MODULE_32) + old_db_schema = sys.modules[SCHEMA_MODULE_32] # Start with db schema that needs migration (version 32) with ( @@ -98,7 +300,7 @@ async def test_migration_changes_prevent_trying_to_migrate_again( patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), patch.object(core, "StateAttributes", old_db_schema.StateAttributes), - patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test(SCHEMA_MODULE_32)), ): async with ( async_test_home_assistant() as hass, diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index b2894883ff2..689441260c7 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -5,6 +5,7 @@ from unittest.mock import PropertyMock import pytest +from homeassistant import core as ha from homeassistant.components.recorder.const import SupportedDialect from homeassistant.components.recorder.db_schema import ( EventData, @@ -18,7 +19,6 @@ from homeassistant.components.recorder.models import ( process_timestamp_to_utc_isoformat, ) from homeassistant.const import EVENT_STATE_CHANGED -import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError from homeassistant.util import dt as dt_util from homeassistant.util.json import json_loads diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index ea764b14401..e5eea0cf89f 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -45,7 +45,7 @@ from .common import ( convert_pending_states_to_meta, ) -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager TEST_EVENT_TYPES = ( "EVENT_TEST_AUTOPURGE", @@ -59,7 +59,7 @@ TEST_EVENT_TYPES = ( @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -112,6 +112,9 @@ async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder) async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old states.""" + assert recorder_mock.states_manager.oldest_ts is None + oldest_ts = recorder_mock.states_manager.oldest_ts + await _add_test_states(hass) # make sure we start with 6 states @@ -127,6 +130,10 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 + assert recorder_mock.states_manager.oldest_ts != oldest_ts + assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts + oldest_ts = recorder_mock.states_manager.oldest_ts + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id purge_before = dt_util.utcnow() - timedelta(days=4) @@ -140,6 +147,8 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> repack=False, ) assert not finished + # states_manager.oldest_ts is not updated until after the purge is complete + assert recorder_mock.states_manager.oldest_ts == oldest_ts with session_scope(hass=hass) as session: states = session.query(States) @@ -162,6 +171,8 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished + # states_manager.oldest_ts should now be updated + assert recorder_mock.states_manager.oldest_ts != oldest_ts with session_scope(hass=hass) as session: states = session.query(States) @@ -169,6 +180,10 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> assert states.count() == 2 assert state_attributes.count() == 1 + assert recorder_mock.states_manager.oldest_ts != oldest_ts + assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts + oldest_ts = recorder_mock.states_manager.oldest_ts + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id # run purge_old_data again @@ -181,6 +196,8 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> repack=False, ) assert not finished + # states_manager.oldest_ts is not updated until after the purge is complete + assert recorder_mock.states_manager.oldest_ts == oldest_ts with session_scope(hass=hass) as session: assert states.count() == 0 diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 2bd1e7fd7f7..45bef68dabd 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -47,12 +47,12 @@ from .db_schema_32 import ( StatisticsShortTerm, ) -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -1027,7 +1027,7 @@ async def test_purge_can_mix_legacy_and_new_format( def _recreate_legacy_events_index(): """Recreate the legacy events index since its no longer created on new instances.""" migration._create_index( - recorder_mock.get_session, "states", "ix_states_event_id" + recorder_mock, recorder_mock.get_session, "states", "ix_states_event_id" ) recorder_mock.use_legacy_events_index = True @@ -1178,7 +1178,7 @@ async def test_purge_can_mix_legacy_and_new_format_with_detached_state( def _recreate_legacy_events_index(): """Recreate the legacy events index since its no longer created on new instances.""" migration._create_index( - recorder_mock.get_session, "states", "ix_states_event_id" + recorder_mock, recorder_mock.get_session, "states", "ix_states_event_id" ) recorder_mock.use_legacy_events_index = True diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 6b1e1a655db..6e192295c58 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -41,7 +41,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( assert_dict_of_states_equal_without_context_and_last_changed, @@ -54,12 +54,12 @@ from .common import ( ) from tests.common import MockPlatform, mock_platform -from tests.typing import RecorderInstanceGenerator, WebSocketGenerator +from tests.typing import RecorderInstanceContextManager, WebSocketGenerator @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index 1f9be0cabee..49b8836af70 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -17,7 +17,7 @@ import pytest from homeassistant.components import recorder from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.util import session_scope -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( CREATE_ENGINE_TARGET, @@ -27,7 +27,7 @@ from .common import ( ) from tests.common import async_test_home_assistant -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager SCHEMA_VERSION_POSTFIX = "23_with_newer_columns" SCHEMA_MODULE = get_schema_module_path(SCHEMA_VERSION_POSTFIX) @@ -37,7 +37,8 @@ SCHEMA_MODULE = get_schema_module_path(SCHEMA_VERSION_POSTFIX) @pytest.mark.usefixtures("skip_by_db_engine") @pytest.mark.parametrize("persistent_database", [True]) async def test_delete_duplicates( - async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture + async_test_recorder: RecorderInstanceContextManager, + caplog: pytest.LogCaptureFixture, ) -> None: """Test removal of duplicated statistics. @@ -224,7 +225,8 @@ async def test_delete_duplicates( @pytest.mark.usefixtures("skip_by_db_engine") @pytest.mark.parametrize("persistent_database", [True]) async def test_delete_duplicates_many( - async_test_recorder: RecorderInstanceGenerator, caplog: pytest.LogCaptureFixture + async_test_recorder: RecorderInstanceContextManager, + caplog: pytest.LogCaptureFixture, ) -> None: """Test removal of duplicated statistics. @@ -418,7 +420,7 @@ async def test_delete_duplicates_many( @pytest.mark.usefixtures("skip_by_db_engine") @pytest.mark.parametrize("persistent_database", [True]) async def test_delete_duplicates_non_identical( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, tmp_path: Path, ) -> None: @@ -613,7 +615,7 @@ async def test_delete_duplicates_non_identical( @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_delete_duplicates_short_term( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, tmp_path: Path, ) -> None: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 99bd5083489..c9020762d4b 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -35,7 +35,6 @@ from homeassistant.components.recorder.models import ( from homeassistant.components.recorder.util import ( MIN_VERSION_SQLITE, RETRYABLE_MYSQL_ERRORS, - UPCOMING_MIN_VERSION_SQLITE, database_job_retry_wrapper, end_incomplete_runs, is_second_sunday, @@ -56,12 +55,12 @@ from .common import ( ) from tests.common import async_test_home_assistant -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager, RecorderInstanceGenerator @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder.""" @@ -236,7 +235,7 @@ def test_setup_connection_for_dialect_mysql(mysql_version) -> None: @pytest.mark.parametrize( "sqlite_version", - [str(UPCOMING_MIN_VERSION_SQLITE)], + [str(MIN_VERSION_SQLITE)], ) def test_setup_connection_for_dialect_sqlite(sqlite_version: str) -> None: """Test setting up the connection for a sqlite dialect.""" @@ -289,7 +288,7 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version: str) -> None: @pytest.mark.parametrize( "sqlite_version", - [str(UPCOMING_MIN_VERSION_SQLITE)], + [str(MIN_VERSION_SQLITE)], ) def test_setup_connection_for_dialect_sqlite_zero_commit_interval( sqlite_version: str, @@ -502,7 +501,7 @@ def test_supported_pgsql(caplog: pytest.LogCaptureFixture, pgsql_version) -> Non assert "minimum supported version" not in caplog.text assert database_engine is not None - assert database_engine.optimizer.slow_range_in_select is False + assert database_engine.optimizer.slow_range_in_select is True @pytest.mark.parametrize( @@ -510,11 +509,11 @@ def test_supported_pgsql(caplog: pytest.LogCaptureFixture, pgsql_version) -> Non [ ( "3.30.0", - "Version 3.30.0 of SQLite is not supported; minimum supported version is 3.31.0.", + "Version 3.30.0 of SQLite is not supported; minimum supported version is 3.40.1.", ), ( "2.0.0", - "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.31.0.", + "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.40.1.", ), ], ) @@ -552,8 +551,8 @@ def test_fail_outdated_sqlite( @pytest.mark.parametrize( "sqlite_version", [ - ("3.31.0"), - ("3.33.0"), + ("3.40.1"), + ("3.41.0"), ], ) def test_supported_sqlite(caplog: pytest.LogCaptureFixture, sqlite_version) -> None: @@ -734,63 +733,6 @@ async def test_no_issue_for_mariadb_with_MDEV_25020( assert database_engine.optimizer.slow_range_in_select is False -async def test_issue_for_old_sqlite( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test we create and delete an issue for old sqlite versions.""" - instance_mock = MagicMock() - instance_mock.hass = hass - execute_args = [] - close_mock = MagicMock() - min_version = str(MIN_VERSION_SQLITE) - - def execute_mock(statement): - nonlocal execute_args - execute_args.append(statement) - - def fetchall_mock(): - nonlocal execute_args - if execute_args[-1] == "SELECT sqlite_version()": - return [[min_version]] - return None - - def _make_cursor_mock(*_): - return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) - - dbapi_connection = MagicMock(cursor=_make_cursor_mock) - - database_engine = await hass.async_add_executor_job( - util.setup_connection_for_dialect, - instance_mock, - "sqlite", - dbapi_connection, - True, - ) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue(DOMAIN, "sqlite_too_old") - assert issue is not None - assert issue.translation_placeholders == { - "min_version": str(UPCOMING_MIN_VERSION_SQLITE), - "server_version": min_version, - } - - min_version = str(UPCOMING_MIN_VERSION_SQLITE) - database_engine = await hass.async_add_executor_job( - util.setup_connection_for_dialect, - instance_mock, - "sqlite", - dbapi_connection, - True, - ) - await hass.async_block_till_done() - - issue = issue_registry.async_get_issue(DOMAIN, "sqlite_too_old") - assert issue is None - assert database_engine is not None - - @pytest.mark.skip_on_db_engine(["mysql", "postgresql"]) @pytest.mark.usefixtures("skip_by_db_engine") async def test_basic_sanity_check( diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index d59486b61f0..c4c1285990d 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -17,20 +17,22 @@ from homeassistant.components.recorder.queries import select_event_type_ids from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import Event, EventOrigin, State -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import async_wait_recording_done from .conftest import instrument_migration from tests.common import async_test_home_assistant -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" SCHEMA_MODULE_30 = "tests.components.recorder.db_schema_30" SCHEMA_MODULE_32 = "tests.components.recorder.db_schema_32" -def _create_engine_test(schema_module: str) -> Callable: +def _create_engine_test( + schema_module: str, *, initial_version: int | None = None +) -> Callable: """Test version of create_engine that initializes with old schema. This simulates an existing db with the old schema. @@ -49,6 +51,10 @@ def _create_engine_test(schema_module: str) -> Callable: session.add( recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) ) + if initial_version is not None: + session.add( + recorder.db_schema.SchemaChanges(schema_version=initial_version) + ) session.add( recorder.db_schema.SchemaChanges( schema_version=old_db_schema.SCHEMA_VERSION @@ -67,10 +73,13 @@ def _create_engine_test(schema_module: str) -> Callable: @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_times( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, ) -> None: - """Test we can migrate times in the events and states tables.""" + """Test we can migrate times in the events and states tables. + + Also tests entity id post migration. + """ importlib.import_module(SCHEMA_MODULE_30) old_db_schema = sys.modules[SCHEMA_MODULE_30] now = dt_util.utcnow() @@ -122,7 +131,13 @@ async def test_migrate_times( patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), - patch(CREATE_ENGINE_TARGET, new=_create_engine_test(SCHEMA_MODULE_30)), + patch( + CREATE_ENGINE_TARGET, + new=_create_engine_test( + SCHEMA_MODULE_30, + initial_version=27, # Set to 27 for the entity id post migration to run + ), + ), ): async with ( async_test_home_assistant() as hass, @@ -225,7 +240,7 @@ async def test_migrate_times( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_can_resume_entity_id_post_migration( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: @@ -274,7 +289,13 @@ async def test_migrate_can_resume_entity_id_post_migration( patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), - patch(CREATE_ENGINE_TARGET, new=_create_engine_test(SCHEMA_MODULE_32)), + patch( + CREATE_ENGINE_TARGET, + new=_create_engine_test( + SCHEMA_MODULE_32, + initial_version=27, # Set to 27 for the entity id post migration to run + ), + ), ): async with ( async_test_home_assistant() as hass, @@ -330,7 +351,7 @@ async def test_migrate_can_resume_entity_id_post_migration( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_migrate_can_resume_ix_states_event_id_removed( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: @@ -394,7 +415,13 @@ async def test_migrate_can_resume_ix_states_event_id_removed( patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), - patch(CREATE_ENGINE_TARGET, new=_create_engine_test(SCHEMA_MODULE_32)), + patch( + CREATE_ENGINE_TARGET, + new=_create_engine_test( + SCHEMA_MODULE_32, + initial_version=27, # Set to 27 for the entity id post migration to run + ), + ), ): async with ( async_test_home_assistant() as hass, @@ -463,7 +490,7 @@ async def test_migrate_can_resume_ix_states_event_id_removed( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_out_of_disk_space_while_rebuild_states_table( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: @@ -527,7 +554,13 @@ async def test_out_of_disk_space_while_rebuild_states_table( patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), - patch(CREATE_ENGINE_TARGET, new=_create_engine_test(SCHEMA_MODULE_32)), + patch( + CREATE_ENGINE_TARGET, + new=_create_engine_test( + SCHEMA_MODULE_32, + initial_version=27, # Set to 27 for the entity id post migration to run + ), + ), ): async with ( async_test_home_assistant() as hass, @@ -637,7 +670,7 @@ async def test_out_of_disk_space_while_rebuild_states_table( @pytest.mark.parametrize("persistent_database", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_out_of_disk_space_while_removing_foreign_key( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, caplog: pytest.LogCaptureFixture, recorder_db_url: str, ) -> None: @@ -705,7 +738,13 @@ async def test_out_of_disk_space_while_removing_foreign_key( patch.object(core, "EventData", old_db_schema.EventData), patch.object(core, "States", old_db_schema.States), patch.object(core, "Events", old_db_schema.Events), - patch(CREATE_ENGINE_TARGET, new=_create_engine_test(SCHEMA_MODULE_32)), + patch( + CREATE_ENGINE_TARGET, + new=_create_engine_test( + SCHEMA_MODULE_32, + initial_version=27, # Set to 27 for the entity id post migration to run + ), + ), ): async with ( async_test_home_assistant() as hass, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 403384aee9f..9e5172ae1f0 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -27,7 +27,7 @@ from homeassistant.components.sensor import UNIT_CONVERTERS from homeassistant.core import HomeAssistant from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from .common import ( @@ -41,7 +41,11 @@ from .common import ( from .conftest import InstrumentedMigration from tests.common import async_fire_time_changed -from tests.typing import RecorderInstanceGenerator, WebSocketGenerator +from tests.typing import ( + RecorderInstanceContextManager, + RecorderInstanceGenerator, + WebSocketGenerator, +) @pytest.fixture @@ -2623,7 +2627,7 @@ async def test_recorder_info_no_instance( async def test_recorder_info_migration_queue_exhausted( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, instrument_migration: InstrumentedMigration, ) -> None: """Test getting recorder status when recorder queue is exhausted.""" diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index c68fe14430a..3ada2d343fe 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, mock_open, patch -import homeassistant.components.remember_the_milk as rtm +from homeassistant.components import remember_the_milk as rtm from homeassistant.core import HomeAssistant from .const import JSON_STRING, PROFILE, TOKEN diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 6c9334aeac4..b4dd513c317 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index c647faba2c1..800d090fd7b 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index 7142608b977..b62cfb4d1b1 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -41,6 +42,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -72,6 +74,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -103,6 +106,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -134,6 +138,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -165,6 +170,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -196,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -304,6 +311,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -341,6 +349,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -372,6 +381,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -403,6 +413,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -434,6 +445,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -465,6 +477,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -496,6 +509,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -527,6 +541,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -558,6 +573,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -690,6 +706,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -727,6 +744,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -758,6 +776,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -789,6 +808,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -860,6 +880,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -897,6 +918,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -928,6 +950,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -959,6 +982,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -990,6 +1014,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1021,6 +1046,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1052,6 +1078,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1083,6 +1110,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1114,6 +1142,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1145,6 +1174,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1288,6 +1318,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -1325,6 +1356,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1356,6 +1388,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1387,6 +1420,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1418,6 +1452,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1449,6 +1484,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1480,6 +1516,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1588,6 +1625,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -1625,6 +1663,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1656,6 +1695,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1687,6 +1727,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1718,6 +1759,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1749,6 +1791,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1780,6 +1823,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1811,6 +1855,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1842,6 +1887,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1974,6 +2020,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -2011,6 +2058,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2042,6 +2090,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2073,6 +2122,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2144,6 +2194,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -2181,6 +2232,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2212,6 +2264,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2243,6 +2296,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2274,6 +2328,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2305,6 +2360,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2336,6 +2392,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2367,6 +2424,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2398,6 +2456,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2429,6 +2488,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index e61255372c1..58789c7aa47 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -41,6 +42,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -88,6 +90,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -125,6 +128,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -156,6 +160,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -187,6 +192,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -256,6 +262,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -293,6 +300,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -324,6 +332,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -355,6 +364,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -424,6 +434,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -461,6 +472,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -492,6 +504,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -523,6 +536,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -592,6 +606,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -629,6 +644,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -676,6 +692,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -713,6 +730,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -744,6 +762,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -775,6 +794,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -844,6 +864,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -881,6 +902,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -912,6 +934,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -943,6 +966,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1012,6 +1036,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -1049,6 +1074,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1080,6 +1106,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1111,6 +1138,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index f90cb92cc63..119defca4ac 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -41,6 +42,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -89,6 +91,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -126,6 +129,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -174,6 +178,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -216,6 +221,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -253,6 +259,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -301,6 +308,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -338,6 +346,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -389,6 +398,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -426,6 +436,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -477,6 +488,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -519,6 +531,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -556,6 +569,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 9974e21be75..526c8af5bc4 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -46,6 +47,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -90,6 +92,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -143,6 +146,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -187,6 +191,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -240,6 +245,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -284,6 +290,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -337,6 +344,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -379,6 +387,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -423,6 +432,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -476,6 +486,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -520,6 +531,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -573,6 +585,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -617,6 +630,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index b092222c9f3..175ad2422ed 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -43,6 +44,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -76,6 +78,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -109,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -140,6 +144,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -171,6 +176,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -202,6 +208,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -314,6 +321,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -353,6 +361,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -395,6 +404,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -428,6 +438,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -461,6 +472,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -500,6 +512,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -533,6 +546,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -566,6 +580,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -599,6 +614,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -630,6 +646,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -663,6 +680,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -696,6 +714,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -729,6 +748,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -760,6 +780,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -791,6 +812,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -822,6 +844,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1071,6 +1094,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -1110,6 +1134,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1152,6 +1177,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1185,6 +1211,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1218,6 +1245,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1257,6 +1285,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1290,6 +1319,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1323,6 +1353,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1356,6 +1387,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1387,6 +1419,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1420,6 +1453,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1453,6 +1487,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1484,6 +1519,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1515,6 +1551,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1546,6 +1583,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1577,6 +1615,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1824,6 +1863,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -1863,6 +1903,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1905,6 +1946,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1938,6 +1980,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1971,6 +2014,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2010,6 +2054,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2043,6 +2088,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2076,6 +2122,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2109,6 +2156,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2140,6 +2188,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2173,6 +2222,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2206,6 +2256,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2237,6 +2288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2268,6 +2320,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2299,6 +2352,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2330,6 +2384,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2361,6 +2416,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2620,6 +2676,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -2659,6 +2716,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2692,6 +2750,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2725,6 +2784,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2756,6 +2816,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -2787,6 +2848,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2818,6 +2880,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -2930,6 +2993,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -2969,6 +3033,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3011,6 +3076,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3044,6 +3110,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3077,6 +3144,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3116,6 +3184,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3149,6 +3218,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3182,6 +3252,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3215,6 +3286,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3246,6 +3318,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -3279,6 +3352,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3312,6 +3386,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3345,6 +3420,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3376,6 +3452,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -3407,6 +3484,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3438,6 +3516,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -3687,6 +3766,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -3726,6 +3806,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3768,6 +3849,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3801,6 +3883,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3834,6 +3917,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3873,6 +3957,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3906,6 +3991,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3939,6 +4025,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3972,6 +4059,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4003,6 +4091,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -4036,6 +4125,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4069,6 +4159,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4100,6 +4191,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4131,6 +4223,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -4162,6 +4255,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4193,6 +4287,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -4440,6 +4535,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -4479,6 +4575,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4521,6 +4618,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4554,6 +4652,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4587,6 +4686,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4626,6 +4726,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4659,6 +4760,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4692,6 +4794,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4725,6 +4828,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4756,6 +4860,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -4789,6 +4894,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4822,6 +4928,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4853,6 +4960,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4884,6 +4992,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -4915,6 +5024,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -4946,6 +5056,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4977,6 +5088,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 81865d98801..2862aa55b4d 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -9,7 +9,11 @@ from reolink_aio.baichuan import Baichuan from reolink_aio.exceptions import ReolinkError from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL -from homeassistant.components.reolink.const import CONF_USE_HTTPS, DOMAIN +from homeassistant.components.reolink.const import ( + CONF_SUPPORTS_PRIVACY_MODE, + CONF_USE_HTTPS, + DOMAIN, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -43,6 +47,7 @@ TEST_HOST_MODEL = "RLN8-410" TEST_ITEM_NUMBER = "P000" TEST_CAM_MODEL = "RLC-123" TEST_DUO_MODEL = "Reolink Duo PoE" +TEST_PRIVACY = True @pytest.fixture @@ -65,6 +70,7 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock = host_mock_class.return_value host_mock.get_host_data.return_value = None host_mock.get_states.return_value = None + host_mock.supported.return_value = True host_mock.check_new_firmware.return_value = False host_mock.unsubscribe.return_value = True host_mock.logout.return_value = True @@ -113,6 +119,9 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]} host_mock.checked_api_versions = {"GetEvents": 1} host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]} + host_mock.get_raw_host_data.return_value = ( + "{'host':'TEST_RESPONSE','channel':'TEST_RESPONSE'}" + ) # enums host_mock.whiteled_mode.return_value = 1 @@ -126,7 +135,9 @@ def reolink_connect_class() -> Generator[MagicMock]: host_mock.baichuan = create_autospec(Baichuan) # Disable tcp push by default for tests host_mock.baichuan.events_active = False + host_mock.baichuan.privacy_mode.return_value = False host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error") + yield host_mock_class @@ -157,6 +168,7 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry: CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: TEST_PORT, CONF_USE_HTTPS: TEST_USE_HTTPS, + CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY, }, options={ CONF_PROTOCOL: DEFAULT_PROTOCOL, diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 59342934c1c..4fe671f8cca 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -2,7 +2,7 @@ import json from typing import Any -from unittest.mock import ANY, AsyncMock, MagicMock, call +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from aiohttp import ClientSession from freezegun.api import FrozenDateTimeFactory @@ -11,14 +11,18 @@ from reolink_aio.exceptions import ( ApiError, CredentialsInvalidError, LoginFirmwareError, + LoginPrivacyModeError, ReolinkError, ) from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL -from homeassistant.components.reolink.const import CONF_USE_HTTPS, DOMAIN +from homeassistant.components.reolink.const import ( + CONF_SUPPORTS_PRIVACY_MODE, + CONF_USE_HTTPS, + DOMAIN, +) from homeassistant.components.reolink.exceptions import ReolinkWebhookException from homeassistant.components.reolink.host import DEFAULT_TIMEOUT from homeassistant.config_entries import ConfigEntryState @@ -32,6 +36,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .conftest import ( DHCP_FORMATTED_MAC, @@ -42,6 +47,7 @@ from .conftest import ( TEST_PASSWORD, TEST_PASSWORD2, TEST_PORT, + TEST_PRIVACY, TEST_USE_HTTPS, TEST_USERNAME, TEST_USERNAME2, @@ -81,6 +87,7 @@ async def test_config_flow_manual_success( CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: TEST_PORT, CONF_USE_HTTPS: TEST_USE_HTTPS, + CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY, } assert result["options"] == { CONF_PROTOCOL: DEFAULT_PROTOCOL, @@ -88,6 +95,60 @@ async def test_config_flow_manual_success( assert result["result"].unique_id == TEST_MAC +async def test_config_flow_privacy_success( + hass: HomeAssistant, reolink_connect: MagicMock, mock_setup_entry: MagicMock +) -> None: + """Successful flow when privacy mode is turned on.""" + reolink_connect.baichuan.privacy_mode.return_value = True + reolink_connect.get_host_data.side_effect = LoginPrivacyModeError("Test error") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is 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 FlowResultType.FORM + assert result["step_id"] == "privacy" + assert result["errors"] is None + + assert reolink_connect.baichuan.set_privacy_mode.call_count == 0 + reolink_connect.get_host_data.reset_mock(side_effect=True) + + with patch("homeassistant.components.reolink.config_flow.API_STARTUP_TIME", new=0): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert reolink_connect.baichuan.set_privacy_mode.call_count == 1 + + assert result["type"] is 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, + CONF_USE_HTTPS: TEST_USE_HTTPS, + CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY, + } + assert result["options"] == { + CONF_PROTOCOL: DEFAULT_PROTOCOL, + } + assert result["result"].unique_id == TEST_MAC + + reolink_connect.baichuan.privacy_mode.return_value = False + + async def test_config_flow_errors( hass: HomeAssistant, reolink_connect: MagicMock, mock_setup_entry: MagicMock ) -> None: @@ -240,6 +301,7 @@ async def test_config_flow_errors( CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: TEST_PORT, CONF_USE_HTTPS: TEST_USE_HTTPS, + CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY, } assert result["options"] == { CONF_PROTOCOL: DEFAULT_PROTOCOL, @@ -381,7 +443,7 @@ async def test_reauth_abort_unique_id_mismatch( async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> None: """Successful flow from DHCP discovery.""" - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST, hostname="Reolink", macaddress=DHCP_FORMATTED_MAC, @@ -411,6 +473,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: TEST_PORT, CONF_USE_HTTPS: TEST_USE_HTTPS, + CONF_SUPPORTS_PRIVACY_MODE: TEST_PRIVACY, } assert result["options"] == { CONF_PROTOCOL: DEFAULT_PROTOCOL, @@ -451,7 +514,7 @@ async def test_dhcp_ip_update_aborted_if_wrong_mac( async_fire_time_changed(hass) await hass.async_block_till_done() - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST2, hostname="Reolink", macaddress=DHCP_FORMATTED_MAC, @@ -548,7 +611,7 @@ async def test_dhcp_ip_update( async_fire_time_changed(hass) await hass.async_block_till_done() - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST2, hostname="Reolink", macaddress=DHCP_FORMATTED_MAC, @@ -620,7 +683,7 @@ async def test_dhcp_ip_update_ingnored_if_still_connected( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - dhcp_data = dhcp.DhcpServiceInfo( + dhcp_data = DhcpServiceInfo( ip=TEST_HOST2, hostname="Reolink", macaddress=DHCP_FORMATTED_MAC, diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index f851e13c91d..28d8c542f4f 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,13 +1,18 @@ """Test the Reolink init.""" import asyncio +from collections.abc import Callable from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest from reolink_aio.api import Chime -from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError +from reolink_aio.exceptions import ( + CredentialsInvalidError, + LoginPrivacyModeError, + ReolinkError, +) from homeassistant.components.reolink import ( DEVICE_UPDATE_INTERVAL, @@ -16,7 +21,13 @@ from homeassistant.components.reolink import ( ) from homeassistant.components.reolink.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_PORT, STATE_OFF, STATE_UNAVAILABLE, Platform +from homeassistant.const import ( + CONF_PORT, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.core_config import async_process_ha_core_config from homeassistant.helpers import ( @@ -749,3 +760,129 @@ async def test_port_changed( await hass.async_block_till_done() assert config_entry.data[CONF_PORT] == 4567 + + +async def test_privacy_mode_on( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, +) -> None: + """Test successful setup even when privacy mode is turned on.""" + reolink_connect.baichuan.privacy_mode.return_value = True + reolink_connect.get_states = AsyncMock( + side_effect=LoginPrivacyModeError("Test error") + ) + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + + reolink_connect.baichuan.privacy_mode.return_value = False + + +async def test_LoginPrivacyModeError( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, +) -> None: + """Test normal update when get_states returns a LoginPrivacyModeError.""" + reolink_connect.baichuan.privacy_mode.return_value = False + reolink_connect.get_states = AsyncMock( + side_effect=LoginPrivacyModeError("Test error") + ) + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + reolink_connect.baichuan.check_subscribe_events.reset_mock() + assert reolink_connect.baichuan.check_subscribe_events.call_count == 0 + + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert reolink_connect.baichuan.check_subscribe_events.call_count >= 1 + + +async def test_privacy_mode_change_callback( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, +) -> None: + """Test privacy mode changed callback.""" + + class callback_mock_class: + callback_func = None + + def register_callback( + self, callback_id: str, callback: Callable[[], None], *args, **key_args + ) -> None: + if callback_id == "privacy_mode_change": + self.callback_func = callback + + callback_mock = callback_mock_class() + + reolink_connect.model = TEST_HOST_MODEL + reolink_connect.baichuan.events_active = True + reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True) + reolink_connect.baichuan.register_callback = callback_mock.register_callback + reolink_connect.baichuan.privacy_mode.return_value = True + reolink_connect.audio_record.return_value = True + reolink_connect.get_states = AsyncMock() + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio" + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # simulate a TCP push callback signaling a privacy mode change + reolink_connect.baichuan.privacy_mode.return_value = False + assert callback_mock.callback_func is not None + callback_mock.callback_func() + + # check that a coordinator update was scheduled. + reolink_connect.get_states.reset_mock() + assert reolink_connect.get_states.call_count == 0 + + freezer.tick(5) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert reolink_connect.get_states.call_count >= 1 + assert hass.states.get(entity_id).state == STATE_ON + + # test cleanup during unloading, first reset to privacy mode ON + reolink_connect.baichuan.privacy_mode.return_value = True + callback_mock.callback_func() + freezer.tick(5) + async_fire_time_changed(hass) + await hass.async_block_till_done() + # now fire the callback again, but unload before refresh took place + reolink_connect.baichuan.privacy_mode.return_value = False + callback_mock.callback_func() + await hass.async_block_till_done() + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_remove( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, +) -> None: + """Test removing of the reolink integration.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_remove(config_entry.entry_id) diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 32afd1f73ca..9c5be08e9b6 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -109,11 +109,17 @@ async def test_resolve( ) assert play_media.mime_type == TEST_MIME_TYPE_MP4 + reolink_connect.is_nvr = False + + play_media = await async_resolve_media( + hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None + ) + assert play_media.mime_type == TEST_MIME_TYPE_MP4 + file_id = ( f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" ) reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) - reolink_connect.is_nvr = False play_media = await async_resolve_media( hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None diff --git a/tests/components/reolink/test_switch.py b/tests/components/reolink/test_switch.py index b2e82040ad4..2b2c33f0e8f 100644 --- a/tests/components/reolink/test_switch.py +++ b/tests/components/reolink/test_switch.py @@ -29,36 +29,209 @@ from .conftest import TEST_CAM_NAME, TEST_NVR_NAME, TEST_UID from tests.common import MockConfigEntry, async_fire_time_changed -async def test_cleanup_hdr_switch( +async def test_switch( hass: HomeAssistant, config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, reolink_connect: MagicMock, - entity_registry: er.EntityRegistry, ) -> None: - """Test cleanup of the HDR switch entity.""" - original_id = f"{TEST_UID}_hdr" - domain = Platform.SWITCH + """Test switch entity.""" + reolink_connect.camera_name.return_value = TEST_CAM_NAME + reolink_connect.audio_record.return_value = True - reolink_connect.channels = [0] - reolink_connect.supported.return_value = True - - entity_registry.async_get_or_create( - domain=domain, - platform=DOMAIN, - unique_id=original_id, - config_entry=config_entry, - suggested_object_id=original_id, - disabled_by=er.RegistryEntryDisabler.USER, - ) - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - - # setup CH 0 and host entities/device - with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) is None + entity_id = f"{Platform.SWITCH}.{TEST_CAM_NAME}_record_audio" + assert hass.states.get(entity_id).state == STATE_ON + + reolink_connect.audio_record.return_value = False + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + # test switch turn on + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + reolink_connect.set_audio.assert_called_with(0, True) + + reolink_connect.set_audio.side_effect = ReolinkError("Test error") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # test switch turn off + reolink_connect.set_audio.reset_mock(side_effect=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + reolink_connect.set_audio.assert_called_with(0, False) + + reolink_connect.set_audio.side_effect = ReolinkError("Test error") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + reolink_connect.set_audio.reset_mock(side_effect=True) + + reolink_connect.camera_online.return_value = False + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + reolink_connect.camera_online.return_value = True + + +async def test_host_switch( + hass: HomeAssistant, + config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + reolink_connect: MagicMock, +) -> None: + """Test host switch entity.""" + reolink_connect.camera_name.return_value = TEST_CAM_NAME + reolink_connect.email_enabled.return_value = True + reolink_connect.is_hub = False + reolink_connect.supported.return_value = True + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_email_on_event" + assert hass.states.get(entity_id).state == STATE_ON + + reolink_connect.email_enabled.return_value = False + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + # test switch turn on + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + reolink_connect.set_email.assert_called_with(None, True) + + reolink_connect.set_email.side_effect = ReolinkError("Test error") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # test switch turn off + reolink_connect.set_email.reset_mock(side_effect=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + reolink_connect.set_email.assert_called_with(None, False) + + reolink_connect.set_email.side_effect = ReolinkError("Test error") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + reolink_connect.set_email.reset_mock(side_effect=True) + + +async def test_chime_switch( + hass: HomeAssistant, + config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + reolink_connect: MagicMock, + test_chime: Chime, +) -> None: + """Test host switch entity.""" + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SWITCH}.test_chime_led" + assert hass.states.get(entity_id).state == STATE_ON + + test_chime.led_state = False + freezer.tick(DEVICE_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_OFF + + # test switch turn on + test_chime.set_option = AsyncMock() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + test_chime.set_option.assert_called_with(led=True) + + test_chime.set_option.side_effect = ReolinkError("Test error") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + # test switch turn off + test_chime.set_option.reset_mock(side_effect=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + test_chime.set_option.assert_called_with(led=False) + + test_chime.set_option.side_effect = ReolinkError("Test error") + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + test_chime.set_option.reset_mock(side_effect=True) @pytest.mark.parametrize( @@ -132,41 +305,6 @@ async def test_cleanup_hub_switches( reolink_connect.supported.return_value = True -async def test_hdr_switch_deprecated_repair_issue( - hass: HomeAssistant, - config_entry: MockConfigEntry, - reolink_connect: MagicMock, - entity_registry: er.EntityRegistry, - issue_registry: ir.IssueRegistry, -) -> None: - """Test repairs issue is raised when hdr switch entity used.""" - original_id = f"{TEST_UID}_hdr" - domain = Platform.SWITCH - - reolink_connect.channels = [0] - reolink_connect.supported.return_value = True - - entity_registry.async_get_or_create( - domain=domain, - platform=DOMAIN, - unique_id=original_id, - config_entry=config_entry, - suggested_object_id=original_id, - disabled_by=None, - ) - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - - # setup CH 0 and host entities/device - with patch("homeassistant.components.reolink.PLATFORMS", [domain]): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) - - assert (DOMAIN, "hdr_switch_deprecated") in issue_registry.issues - - @pytest.mark.parametrize( ( "original_id", @@ -238,205 +376,3 @@ async def test_hub_switches_repair_issue( reolink_connect.is_hub = False reolink_connect.supported.return_value = True - - -async def test_switch( - hass: HomeAssistant, - config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, -) -> None: - """Test switch entity.""" - reolink_connect.camera_name.return_value = TEST_CAM_NAME - - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.LOADED - - entity_id = f"{Platform.SWITCH}.{TEST_CAM_NAME}_record" - assert hass.states.get(entity_id).state == STATE_ON - - reolink_connect.recording_enabled.return_value = False - freezer.tick(DEVICE_UPDATE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert hass.states.get(entity_id).state == STATE_OFF - - # test switch turn on - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - reolink_connect.set_recording.assert_called_with(0, True) - - reolink_connect.set_recording.side_effect = ReolinkError("Test error") - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - # test switch turn off - reolink_connect.set_recording.reset_mock(side_effect=True) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - reolink_connect.set_recording.assert_called_with(0, False) - - reolink_connect.set_recording.side_effect = ReolinkError("Test error") - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - reolink_connect.set_recording.reset_mock(side_effect=True) - - reolink_connect.camera_online.return_value = False - freezer.tick(DEVICE_UPDATE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - reolink_connect.camera_online.return_value = True - - -async def test_host_switch( - hass: HomeAssistant, - config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, -) -> None: - """Test host switch entity.""" - reolink_connect.camera_name.return_value = TEST_CAM_NAME - reolink_connect.recording_enabled.return_value = True - - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.LOADED - - entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record" - assert hass.states.get(entity_id).state == STATE_ON - - reolink_connect.recording_enabled.return_value = False - freezer.tick(DEVICE_UPDATE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert hass.states.get(entity_id).state == STATE_OFF - - # test switch turn on - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - reolink_connect.set_recording.assert_called_with(None, True) - - reolink_connect.set_recording.side_effect = ReolinkError("Test error") - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - # test switch turn off - reolink_connect.set_recording.reset_mock(side_effect=True) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - reolink_connect.set_recording.assert_called_with(None, False) - - reolink_connect.set_recording.side_effect = ReolinkError("Test error") - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - reolink_connect.set_recording.reset_mock(side_effect=True) - - -async def test_chime_switch( - hass: HomeAssistant, - config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, - test_chime: Chime, -) -> None: - """Test host switch entity.""" - with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.LOADED - - entity_id = f"{Platform.SWITCH}.test_chime_led" - assert hass.states.get(entity_id).state == STATE_ON - - test_chime.led_state = False - freezer.tick(DEVICE_UPDATE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert hass.states.get(entity_id).state == STATE_OFF - - # test switch turn on - test_chime.set_option = AsyncMock() - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - test_chime.set_option.assert_called_with(led=True) - - test_chime.set_option.side_effect = ReolinkError("Test error") - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - # test switch turn off - test_chime.set_option.reset_mock(side_effect=True) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - test_chime.set_option.assert_called_with(led=False) - - test_chime.set_option.side_effect = ReolinkError("Test error") - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - test_chime.set_option.reset_mock(side_effect=True) diff --git a/tests/components/reolink/test_util.py b/tests/components/reolink/test_util.py new file mode 100644 index 00000000000..f66f4682b98 --- /dev/null +++ b/tests/components/reolink/test_util.py @@ -0,0 +1,115 @@ +"""Test the Reolink util functions.""" + +from unittest.mock import MagicMock, patch + +import pytest +from reolink_aio.exceptions import ( + ApiError, + CredentialsInvalidError, + InvalidContentTypeError, + InvalidParameterError, + LoginError, + NoDataError, + NotSupportedError, + ReolinkConnectionError, + ReolinkError, + ReolinkTimeoutError, + SubscriptionError, + UnexpectedDataError, +) + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + +from .conftest import TEST_NVR_NAME + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("side_effect", "expected"), + [ + ( + ApiError("Test error"), + HomeAssistantError, + ), + ( + CredentialsInvalidError("Test error"), + HomeAssistantError, + ), + ( + InvalidContentTypeError("Test error"), + HomeAssistantError, + ), + ( + InvalidParameterError("Test error"), + ServiceValidationError, + ), + ( + LoginError("Test error"), + HomeAssistantError, + ), + ( + NoDataError("Test error"), + HomeAssistantError, + ), + ( + NotSupportedError("Test error"), + HomeAssistantError, + ), + ( + ReolinkConnectionError("Test error"), + HomeAssistantError, + ), + ( + ReolinkError("Test error"), + HomeAssistantError, + ), + ( + ReolinkTimeoutError("Test error"), + HomeAssistantError, + ), + ( + SubscriptionError("Test error"), + HomeAssistantError, + ), + ( + UnexpectedDataError("Test error"), + HomeAssistantError, + ), + ], +) +async def test_try_function( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + side_effect: ReolinkError, + expected: Exception, +) -> None: + """Test try_function error translations using number entity.""" + reolink_connect.volume.return_value = 80 + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.NUMBER]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.NUMBER}.{TEST_NVR_NAME}_volume" + + reolink_connect.set_volume.side_effect = side_effect + with pytest.raises(expected): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50}, + blocking=True, + ) + + reolink_connect.set_volume.reset_mock(side_effect=True) diff --git a/tests/components/reolink/test_views.py b/tests/components/reolink/test_views.py new file mode 100644 index 00000000000..c994cc59c5d --- /dev/null +++ b/tests/components/reolink/test_views.py @@ -0,0 +1,243 @@ +"""Tests for the Reolink views platform.""" + +from http import HTTPStatus +import logging +from typing import Any, cast +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +from aiohttp import ClientConnectionError, ClientResponse +import pytest +from reolink_aio.enums import VodRequestType +from reolink_aio.exceptions import ReolinkError + +from homeassistant.components.reolink.views import async_generate_playback_proxy_url +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator + +TEST_YEAR = 2023 +TEST_MONTH = 11 +TEST_DAY = 14 +TEST_DAY2 = 15 +TEST_HOUR = 13 +TEST_MINUTE = 12 +TEST_FILE_NAME_MP4 = f"Mp4Record/{TEST_YEAR}-{TEST_MONTH}-{TEST_DAY}/RecS04_{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00_123456_AB123C.mp4" +TEST_STREAM = "sub" +TEST_CHANNEL = "0" +TEST_VOD_TYPE = VodRequestType.PLAYBACK.value +TEST_MIME_TYPE_MP4 = "video/mp4" +TEST_URL = "http://test_url&token=test" +TEST_ERROR = "TestError" + + +def get_mock_session( + response: list[Any] | None = None, + content_length: int = 8, + content_type: str = TEST_MIME_TYPE_MP4, +) -> Mock: + """Get a mock session to mock the camera response.""" + if response is None: + response = [b"test", b"test", StopAsyncIteration()] + + content = Mock() + content.__anext__ = AsyncMock(side_effect=response) + content.__aiter__ = Mock(return_value=content) + + mock_response = Mock() + mock_response.content_length = content_length + mock_response.content_type = content_type + mock_response.content.iter_chunked = Mock(return_value=content) + + mock_session = Mock() + mock_session.get = AsyncMock(return_value=mock_response) + return mock_session + + +async def test_playback_proxy( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test successful playback proxy URL.""" + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) + + mock_session = get_mock_session() + + with patch( + "homeassistant.components.reolink.views.async_get_clientsession", + return_value=mock_session, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + caplog.set_level(logging.DEBUG) + + proxy_url = async_generate_playback_proxy_url( + config_entry.entry_id, + TEST_CHANNEL, + TEST_FILE_NAME_MP4, + TEST_STREAM, + TEST_VOD_TYPE, + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(proxy_url)) + + assert await response.content.read() == b"testtest" + assert response.status == 200 + + +async def test_proxy_get_source_error( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test error while getting source for playback proxy URL.""" + reolink_connect.get_vod_source.side_effect = ReolinkError(TEST_ERROR) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + proxy_url = async_generate_playback_proxy_url( + config_entry.entry_id, + TEST_CHANNEL, + TEST_FILE_NAME_MP4, + TEST_STREAM, + TEST_VOD_TYPE, + ) + + http_client = await hass_client() + response = await http_client.get(proxy_url) + + assert await response.content.read() == bytes(TEST_ERROR, "utf-8") + assert response.status == HTTPStatus.BAD_REQUEST + reolink_connect.get_vod_source.side_effect = None + + +async def test_proxy_invalid_config_entry_id( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test config entry id not found for playback proxy URL.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + proxy_url = async_generate_playback_proxy_url( + "wrong_config_id", + TEST_CHANNEL, + TEST_FILE_NAME_MP4, + TEST_STREAM, + TEST_VOD_TYPE, + ) + + http_client = await hass_client() + response = await http_client.get(proxy_url) + + assert await response.content.read() == bytes( + "Reolink playback proxy could not find config entry id: wrong_config_id", + "utf-8", + ) + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_playback_proxy_timeout( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test playback proxy URL with a timeout in the second chunk.""" + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) + + mock_session = get_mock_session([b"test", TimeoutError()], 4) + + with patch( + "homeassistant.components.reolink.views.async_get_clientsession", + return_value=mock_session, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + proxy_url = async_generate_playback_proxy_url( + config_entry.entry_id, + TEST_CHANNEL, + TEST_FILE_NAME_MP4, + TEST_STREAM, + TEST_VOD_TYPE, + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(proxy_url)) + + assert await response.content.read() == b"test" + assert response.status == 200 + + +async def test_playback_wrong_content( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test playback proxy URL with a wrong content type in the response.""" + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) + + mock_session = get_mock_session(content_type="video/x-flv") + + with patch( + "homeassistant.components.reolink.views.async_get_clientsession", + return_value=mock_session, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + proxy_url = async_generate_playback_proxy_url( + config_entry.entry_id, + TEST_CHANNEL, + TEST_FILE_NAME_MP4, + TEST_STREAM, + TEST_VOD_TYPE, + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(proxy_url)) + + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_playback_connect_error( + hass: HomeAssistant, + reolink_connect: MagicMock, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test playback proxy URL with a connection error.""" + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) + + mock_session = Mock() + mock_session.get = AsyncMock(side_effect=ClientConnectionError(TEST_ERROR)) + + with patch( + "homeassistant.components.reolink.views.async_get_clientsession", + return_value=mock_session, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + proxy_url = async_generate_playback_proxy_url( + config_entry.entry_id, + TEST_CHANNEL, + TEST_FILE_NAME_MP4, + TEST_STREAM, + TEST_VOD_TYPE, + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(proxy_url)) + + assert response.status == HTTPStatus.BAD_REQUEST diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 2e02063b215..d5fc5eca55c 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -591,7 +591,7 @@ async def test_update_with_no_template(hass: HomeAssistant) -> None: assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1 state = hass.states.get("sensor.foo") - assert state.state == '{"key": "some_json_value"}' + assert state.state == '{"key":"some_json_value"}' @respx.mock diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 9329edb3a00..fd113bceaa0 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import CoreState, HomeAssistant, State, callback -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .test_init import mock_rflink diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 8f212b6e976..79736d418b5 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -58,17 +58,17 @@ async def test_one_pt2262(hass: HomeAssistant, rfxtrx) -> None: await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("binary_sensor.pt2262_22670e") + state = hass.states.get("binary_sensor.pt2262_226700") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "PT2262 22670e" + assert state.attributes.get("friendly_name") == "PT2262 226700" await rfxtrx.signal("0913000022670e013970") - state = hass.states.get("binary_sensor.pt2262_22670e") + state = hass.states.get("binary_sensor.pt2262_226700") assert state.state == "on" await rfxtrx.signal("09130000226707013d70") - state = hass.states.get("binary_sensor.pt2262_22670e") + state = hass.states.get("binary_sensor.pt2262_226700") assert state.state == "off" @@ -85,10 +85,10 @@ async def test_pt2262_unconfigured(hass: HomeAssistant, rfxtrx) -> None: await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("binary_sensor.pt2262_22670e") + state = hass.states.get("binary_sensor.pt2262_226707") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "PT2262 22670e" + assert state.attributes.get("friendly_name") == "PT2262 226707" state = hass.states.get("binary_sensor.pt2262_226707") assert state @@ -318,7 +318,7 @@ async def test_pt2262_duplicate_id(hass: HomeAssistant, rfxtrx) -> None: await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("binary_sensor.pt2262_22670e") + state = hass.states.get("binary_sensor.pt2262_226700") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "PT2262 22670e" + assert state.attributes.get("friendly_name") == "PT2262 226700" diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 1e23bdaf982..6fd4fc14bc5 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -726,7 +726,6 @@ async def test_options_add_and_configure_device( result["flow_id"], user_input={ "data_bits": 4, - "off_delay": "abcdef", "command_on": "xyz", "command_off": "xyz", }, @@ -735,7 +734,6 @@ async def test_options_add_and_configure_device( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "set_device_options" assert result["errors"] - assert result["errors"]["off_delay"] == "invalid_input_off_delay" assert result["errors"]["command_on"] == "invalid_input_2262_on" assert result["errors"]["command_off"] == "invalid_input_2262_off" @@ -745,7 +743,7 @@ async def test_options_add_and_configure_device( "data_bits": 4, "command_on": "0xE", "command_off": "0x7", - "off_delay": "9", + "off_delay": 9, }, ) @@ -758,10 +756,10 @@ async def test_options_add_and_configure_device( assert entry.data["devices"]["0913000022670e013970"] assert entry.data["devices"]["0913000022670e013970"]["off_delay"] == 9 - state = hass.states.get("binary_sensor.pt2262_22670e") + state = hass.states.get("binary_sensor.pt2262_226700") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "PT2262 22670e" + assert state.attributes.get("friendly_name") == "PT2262 226700" device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 4336798768f..f17fd8743f1 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -330,10 +330,10 @@ async def test_rssi_sensor(hass: HomeAssistant, rfxtrx) -> None: await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("sensor.pt2262_22670e_signal_strength") + state = hass.states.get("sensor.pt2262_226700_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "PT2262 22670e Signal strength" + assert state.attributes.get("friendly_name") == "PT2262 226700 Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -351,7 +351,7 @@ async def test_rssi_sensor(hass: HomeAssistant, rfxtrx) -> None: await rfxtrx.signal("0913000022670e013b70") await rfxtrx.signal("0b1100cd0213c7f230010f71") - state = hass.states.get("sensor.pt2262_22670e_signal_strength") + state = hass.states.get("sensor.pt2262_226700_signal_strength") assert state assert state.state == "-64" @@ -362,7 +362,7 @@ async def test_rssi_sensor(hass: HomeAssistant, rfxtrx) -> None: await rfxtrx.signal("0913000022670e013b60") await rfxtrx.signal("0b1100cd0213c7f230010f61") - state = hass.states.get("sensor.pt2262_22670e_signal_strength") + state = hass.states.get("sensor.pt2262_226700_signal_strength") assert state assert state.state == "-72" diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 7acc008cc8a..964c5ccb2e6 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -70,23 +70,23 @@ async def test_one_pt2262_switch(hass: HomeAssistant, rfxtrx) -> None: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("switch.pt2262_22670e") + state = hass.states.get("switch.pt2262_226700") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "PT2262 22670e" + assert state.attributes.get("friendly_name") == "PT2262 226700" await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.pt2262_22670e"}, blocking=True + "switch", "turn_on", {"entity_id": "switch.pt2262_226700"}, blocking=True ) - state = hass.states.get("switch.pt2262_22670e") + state = hass.states.get("switch.pt2262_226700") assert state.state == "on" await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.pt2262_22670e"}, blocking=True + "switch", "turn_off", {"entity_id": "switch.pt2262_226700"}, blocking=True ) - state = hass.states.get("switch.pt2262_22670e") + state = hass.states.get("switch.pt2262_226700") assert state.state == "off" assert rfxtrx.transport.send.mock_calls == [ @@ -220,26 +220,26 @@ async def test_pt2262_switch_events(hass: HomeAssistant, rfxtrx) -> None: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("switch.pt2262_22670e") + state = hass.states.get("switch.pt2262_226700") assert state assert state.state == STATE_UNKNOWN - assert state.attributes.get("friendly_name") == "PT2262 22670e" + assert state.attributes.get("friendly_name") == "PT2262 226700" # "Command: 0xE" await rfxtrx.signal("0913000022670e013970") - assert hass.states.get("switch.pt2262_22670e").state == "on" + assert hass.states.get("switch.pt2262_226700").state == "on" # "Command: 0x0" await rfxtrx.signal("09130000226700013970") - assert hass.states.get("switch.pt2262_22670e").state == "on" + assert hass.states.get("switch.pt2262_226700").state == "on" # "Command: 0x7" await rfxtrx.signal("09130000226707013d70") - assert hass.states.get("switch.pt2262_22670e").state == "off" + assert hass.states.get("switch.pt2262_226700").state == "off" # "Command: 0x1" await rfxtrx.signal("09130000226701013d70") - assert hass.states.get("switch.pt2262_22670e").state == "off" + assert hass.states.get("switch.pt2262_226700").state == "off" async def test_discover_switch(hass: HomeAssistant, rfxtrx_automatic) -> None: diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 22fa1c2bf32..e7af1d94855 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -6,6 +6,7 @@ from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.ring import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er, translation from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -35,3 +36,62 @@ async def setup_automation(hass: HomeAssistant, alias: str, entity_id: str) -> N } }, ) + + +async def async_check_entity_translations( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + config_entry_id: str, + platform_domain: str, +) -> None: + """Check that entity translations are used correctly. + + Check no unused translations in strings. + Check no translation_key defined when translation not in strings. + Check no translation defined when device class translation can be used. + """ + entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) + + assert entity_entries + assert len({entity_entry.domain for entity_entry in entity_entries}) == 1, ( + "Limit the loaded platforms to 1 platform." + ) + + translations = await translation.async_get_translations( + hass, "en", "entity", [DOMAIN] + ) + device_class_translations = await translation.async_get_translations( + hass, "en", "entity_component", [platform_domain] + ) + unique_device_classes = set() + used_translation_keys = set() + for entity_entry in entity_entries: + dc_translation = None + if entity_entry.original_device_class: + dc_translation_key = f"component.{platform_domain}.entity_component.{entity_entry.original_device_class.value}.name" + dc_translation = device_class_translations.get(dc_translation_key) + + if entity_entry.translation_key: + key = f"component.{DOMAIN}.entity.{entity_entry.domain}.{entity_entry.translation_key}.name" + entity_translation = translations.get(key) + assert entity_translation, ( + f"Translation key {entity_entry.translation_key} defined for {entity_entry.entity_id} not in strings.json" + ) + assert dc_translation != entity_translation, ( + f"Translation {key} is defined the same as the device class translation." + ) + used_translation_keys.add(key) + + else: + unique_key = (entity_entry.device_id, entity_entry.original_device_class) + assert unique_key not in unique_device_classes, ( + f"No translation key and multiple entities using {entity_entry.original_device_class}" + ) + unique_device_classes.add(entity_entry.original_device_class) + + for defined_key in translations: + if defined_key.split(".")[3] != platform_domain: + continue + assert defined_key in used_translation_keys, ( + f"Translation key {defined_key} unused." + ) diff --git a/tests/components/ring/snapshots/test_binary_sensor.ambr b/tests/components/ring/snapshots/test_binary_sensor.ambr index 2f8e4d8a219..09dab9b0ecc 100644 --- a/tests/components/ring/snapshots/test_binary_sensor.ambr +++ b/tests/components/ring/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -75,7 +77,7 @@ 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'motion', + 'translation_key': None, 'unique_id': '987654-motion', 'unit_of_measurement': None, }) @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -123,7 +126,7 @@ 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'motion', + 'translation_key': None, 'unique_id': '765432-motion', 'unit_of_measurement': None, }) @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -219,7 +224,7 @@ 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'motion', + 'translation_key': None, 'unique_id': '345678-motion', 'unit_of_measurement': None, }) diff --git a/tests/components/ring/snapshots/test_button.ambr b/tests/components/ring/snapshots/test_button.ambr index 01f6525450b..7da11d66194 100644 --- a/tests/components/ring/snapshots/test_button.ambr +++ b/tests/components/ring/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/snapshots/test_camera.ambr b/tests/components/ring/snapshots/test_camera.ambr index ec285b438b3..8c3b8a083b0 100644 --- a/tests/components/ring/snapshots/test_camera.ambr +++ b/tests/components/ring/snapshots/test_camera.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -112,6 +114,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -164,6 +167,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -217,6 +221,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -270,6 +275,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/snapshots/test_event.ambr b/tests/components/ring/snapshots/test_event.ambr index e97a01516bb..9c0fee906a0 100644 --- a/tests/components/ring/snapshots/test_event.ambr +++ b/tests/components/ring/snapshots/test_event.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -122,6 +124,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -178,6 +181,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -234,6 +238,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -290,6 +295,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/snapshots/test_light.ambr b/tests/components/ring/snapshots/test_light.ambr index 73874fda259..6c6effb93c1 100644 --- a/tests/components/ring/snapshots/test_light.ambr +++ b/tests/components/ring/snapshots/test_light.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/snapshots/test_number.ambr b/tests/components/ring/snapshots/test_number.ambr index 0873319b837..abc63051f6a 100644 --- a/tests/components/ring/snapshots/test_number.ambr +++ b/tests/components/ring/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -123,6 +125,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -179,6 +182,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -235,6 +239,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -291,6 +296,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -347,6 +353,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/snapshots/test_sensor.ambr b/tests/components/ring/snapshots/test_sensor.ambr index 9fd1ac7ba84..615bd1df018 100644 --- a/tests/components/ring/snapshots/test_sensor.ambr +++ b/tests/components/ring/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -117,11 +120,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wi-Fi signal strength', + 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wifi_signal_strength', + 'translation_key': None, 'unique_id': '123456-wifi_signal_strength', 'unit_of_measurement': 'dBm', }) @@ -131,7 +134,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Ring.com', 'device_class': 'signal_strength', - 'friendly_name': 'Downstairs Wi-Fi signal strength', + 'friendly_name': 'Downstairs Signal strength', 'unit_of_measurement': 'dBm', }), 'context': , @@ -151,6 +154,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -203,6 +207,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -253,6 +258,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +300,104 @@ 'state': 'unknown', }) # --- +# name: test_states[sensor.front_door_last_ding-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.front_door_last_ding', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last ding', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_ding', + 'unique_id': '987654-last_ding', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.front_door_last_ding-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'device_class': 'timestamp', + 'friendly_name': 'Front Door Last ding', + }), + 'context': , + 'entity_id': 'sensor.front_door_last_ding', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[sensor.front_door_last_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.front_door_last_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last motion', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_motion', + 'unique_id': '987654-last_motion', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.front_door_last_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'device_class': 'timestamp', + 'friendly_name': 'Front Door Last motion', + }), + 'context': , + 'entity_id': 'sensor.front_door_last_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_states[sensor.front_door_volume-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -301,6 +405,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -348,6 +453,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -395,6 +501,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -412,11 +519,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wi-Fi signal strength', + 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wifi_signal_strength', + 'translation_key': None, 'unique_id': '987654-wifi_signal_strength', 'unit_of_measurement': 'dBm', }) @@ -426,7 +533,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Ring.com', 'device_class': 'signal_strength', - 'friendly_name': 'Front Door Wi-Fi signal strength', + 'friendly_name': 'Front Door Signal strength', 'unit_of_measurement': 'dBm', }), 'context': , @@ -444,6 +551,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -485,6 +593,104 @@ 'state': 'unknown', }) # --- +# name: test_states[sensor.front_last_ding-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.front_last_ding', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last ding', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_ding', + 'unique_id': '765432-last_ding', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.front_last_ding-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'device_class': 'timestamp', + 'friendly_name': 'Front Last ding', + }), + 'context': , + 'entity_id': 'sensor.front_last_ding', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[sensor.front_last_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.front_last_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last motion', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_motion', + 'unique_id': '765432-last_motion', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.front_last_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'device_class': 'timestamp', + 'friendly_name': 'Front Last motion', + }), + 'context': , + 'entity_id': 'sensor.front_last_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_states[sensor.front_wifi_signal_category-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -492,6 +698,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -539,6 +746,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -556,11 +764,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wi-Fi signal strength', + 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wifi_signal_strength', + 'translation_key': None, 'unique_id': '765432-wifi_signal_strength', 'unit_of_measurement': 'dBm', }) @@ -570,7 +778,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Ring.com', 'device_class': 'signal_strength', - 'friendly_name': 'Front Wi-Fi signal strength', + 'friendly_name': 'Front Signal strength', 'unit_of_measurement': 'dBm', }), 'context': , @@ -590,6 +798,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -640,6 +849,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -687,6 +897,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -735,6 +946,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -782,6 +994,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -829,6 +1042,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -876,6 +1090,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -893,11 +1108,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wi-Fi signal strength', + 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wifi_signal_strength', + 'translation_key': None, 'unique_id': '185036587-wifi_signal_strength', 'unit_of_measurement': 'dBm', }) @@ -907,7 +1122,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Ring.com', 'device_class': 'signal_strength', - 'friendly_name': 'Ingress Wi-Fi signal strength', + 'friendly_name': 'Ingress Signal strength', 'unit_of_measurement': 'dBm', }), 'context': , @@ -927,6 +1142,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -977,6 +1193,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1018,6 +1235,104 @@ 'state': 'unknown', }) # --- +# name: test_states[sensor.internal_last_ding-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.internal_last_ding', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last ding', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_ding', + 'unique_id': '345678-last_ding', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.internal_last_ding-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'device_class': 'timestamp', + 'friendly_name': 'Internal Last ding', + }), + 'context': , + 'entity_id': 'sensor.internal_last_ding', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[sensor.internal_last_motion-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.internal_last_motion', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last motion', + 'platform': 'ring', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_motion', + 'unique_id': '345678-last_motion', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.internal_last_motion-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Ring.com', + 'device_class': 'timestamp', + 'friendly_name': 'Internal Last motion', + }), + 'context': , + 'entity_id': 'sensor.internal_last_motion', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_states[sensor.internal_wifi_signal_category-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1025,6 +1340,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1072,6 +1388,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1089,11 +1406,11 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Wi-Fi signal strength', + 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'wifi_signal_strength', + 'translation_key': None, 'unique_id': '345678-wifi_signal_strength', 'unit_of_measurement': 'dBm', }) @@ -1103,7 +1420,7 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Ring.com', 'device_class': 'signal_strength', - 'friendly_name': 'Internal Wi-Fi signal strength', + 'friendly_name': 'Internal Signal strength', 'unit_of_measurement': 'dBm', }), 'context': , diff --git a/tests/components/ring/snapshots/test_siren.ambr b/tests/components/ring/snapshots/test_siren.ambr index c49ab2cb30f..8ef08815a1e 100644 --- a/tests/components/ring/snapshots/test_siren.ambr +++ b/tests/components/ring/snapshots/test_siren.ambr @@ -11,6 +11,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -63,6 +64,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -111,6 +113,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/snapshots/test_switch.ambr b/tests/components/ring/snapshots/test_switch.ambr index 57c27cfedfa..8c7c55d5169 100644 --- a/tests/components/ring/snapshots/test_switch.ambr +++ b/tests/components/ring/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 81d7d6e6687..c588b022265 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -18,7 +18,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component -from .common import MockConfigEntry, setup_automation, setup_platform +from .common import ( + MockConfigEntry, + async_check_entity_translations, + setup_automation, + setup_platform, +) from .device_mocks import ( FRONT_DEVICE_ID, FRONT_DOOR_DEVICE_ID, @@ -67,6 +72,9 @@ async def test_states( ) -> None: """Test states.""" await setup_platform(hass, Platform.BINARY_SENSOR) + await async_check_entity_translations( + hass, entity_registry, mock_config_entry.entry_id, BINARY_SENSOR_DOMAIN + ) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py index 4b4f019fdf7..54638df9a46 100644 --- a/tests/components/ring/test_camera.py +++ b/tests/components/ring/test_camera.py @@ -436,9 +436,9 @@ async def test_camera_webrtc( assert response assert response.get("success") is False assert response["error"]["code"] == "home_assistant_error" - msg = "The sdp_m_line_index is required for ring webrtc streaming" - assert msg in response["error"].get("message") - assert msg in caplog.text + error_msg = f"Error negotiating stream for {front_camera_mock.name}" + assert error_msg in response["error"].get("message") + assert error_msg in caplog.text front_camera_mock.on_webrtc_candidate.assert_called_once() # Answer message diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 409cdac55aa..778bad67d77 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -6,12 +6,12 @@ import pytest import ring_doorbell from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.ring import DOMAIN from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .conftest import MOCK_HARDWARE_ID @@ -269,9 +269,7 @@ async def test_dhcp_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip=ip_address, macaddress=mac_address, hostname=hostname - ), + data=DhcpServiceInfo(ip=ip_address, macaddress=mac_address, hostname=hostname), ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -302,9 +300,7 @@ async def test_dhcp_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip=ip_address, macaddress=mac_address, hostname=hostname - ), + data=DhcpServiceInfo(ip=ip_address, macaddress=mac_address, hostname=hostname), ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 27d4813f02d..66decb5ce15 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -16,7 +16,7 @@ from homeassistant.components.ring.const import ( CONF_LISTEN_CREDENTIALS, SCAN_INTERVAL, ) -from homeassistant.components.ring.coordinator import RingEventListener +from homeassistant.components.ring.coordinator import RingConfigEntry, RingEventListener from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_DEVICE_ID, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -80,12 +80,12 @@ async def test_auth_failed_on_setup( ("error_type", "log_msg"), [ ( - RingTimeout, - "Timeout communicating with API: ", + RingTimeout("Some internal error info"), + "Timeout communicating with Ring API", ), ( - RingError, - "Error communicating with API: ", + RingError("Some internal error info"), + "Error communicating with Ring API", ), ], ids=["timeout-error", "other-error"], @@ -95,6 +95,7 @@ async def test_error_on_setup( mock_ring_client, mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, error_type, log_msg, ) -> None: @@ -166,11 +167,11 @@ async def test_auth_failure_on_device_update( [ ( RingTimeout, - "Error fetching devices data: Timeout communicating with API: ", + "Error fetching devices data: Timeout communicating with Ring API", ), ( RingError, - "Error fetching devices data: Error communicating with API: ", + "Error fetching devices data: Error communicating with Ring API", ), ], ids=["timeout-error", "other-error"], @@ -178,7 +179,7 @@ async def test_auth_failure_on_device_update( async def test_error_on_global_update( hass: HomeAssistant, mock_ring_client, - mock_config_entry: MockConfigEntry, + mock_config_entry: RingConfigEntry, freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, error_type, @@ -189,15 +190,35 @@ async def test_error_on_global_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_ring_client.async_update_devices.side_effect = error_type + coordinator = mock_config_entry.runtime_data.devices_coordinator + assert coordinator - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) + with patch.object( + coordinator, "_async_update_data", wraps=coordinator._async_update_data + ) as refresh_spy: + error = error_type("Some internal error info 1") + mock_ring_client.async_update_devices.side_effect = error - assert log_msg in caplog.text + freezer.tick(SCAN_INTERVAL * 2) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) - assert hass.config_entries.async_get_entry(mock_config_entry.entry_id) + refresh_spy.assert_called() + assert coordinator.last_exception.__cause__ == error + assert log_msg in caplog.text + + # Check log is not being spammed. + refresh_spy.reset_mock() + error2 = error_type("Some internal error info 2") + caplog.clear() + mock_ring_client.async_update_devices.side_effect = error2 + freezer.tick(SCAN_INTERVAL * 2) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + refresh_spy.assert_called() + assert coordinator.last_exception.__cause__ == error2 + assert log_msg not in caplog.text @pytest.mark.parametrize( @@ -205,11 +226,11 @@ async def test_error_on_global_update( [ ( RingTimeout, - "Error fetching devices data: Timeout communicating with API for device Front: ", + "Error fetching devices data: Timeout communicating with Ring API", ), ( RingError, - "Error fetching devices data: Error communicating with API for device Front: ", + "Error fetching devices data: Error communicating with Ring API", ), ], ids=["timeout-error", "other-error"], @@ -218,7 +239,7 @@ async def test_error_on_device_update( hass: HomeAssistant, mock_ring_client, mock_ring_devices, - mock_config_entry: MockConfigEntry, + mock_config_entry: RingConfigEntry, freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, error_type, @@ -229,15 +250,36 @@ async def test_error_on_device_update( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - front_door_doorbell = mock_ring_devices.get_device(765432) - front_door_doorbell.async_history.side_effect = error_type + coordinator = mock_config_entry.runtime_data.devices_coordinator + assert coordinator - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) + with patch.object( + coordinator, "_async_update_data", wraps=coordinator._async_update_data + ) as refresh_spy: + error = error_type("Some internal error info 1") + front_door_doorbell = mock_ring_devices.get_device(765432) + front_door_doorbell.async_history.side_effect = error - assert log_msg in caplog.text - assert hass.config_entries.async_get_entry(mock_config_entry.entry_id) + freezer.tick(SCAN_INTERVAL * 2) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + refresh_spy.assert_called() + assert coordinator.last_exception.__cause__ == error + assert log_msg in caplog.text + + # Check log is not being spammed. + error2 = error_type("Some internal error info 2") + front_door_doorbell.async_history.side_effect = error2 + refresh_spy.reset_mock() + caplog.clear() + freezer.tick(SCAN_INTERVAL * 2) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + refresh_spy.assert_called() + assert coordinator.last_exception.__cause__ == error2 + assert log_msg not in caplog.text @pytest.mark.parametrize( @@ -444,6 +486,7 @@ async def test_no_listen_start( version=1, data={"username": "foo", "token": {}}, ) + mock_entry.add_to_hass(hass) # Create a binary sensor entity so it is not ignored by the deprecation check # and the listener will start entity_registry.async_get_or_create( @@ -457,7 +500,6 @@ async def test_no_listen_start( mock_ring_event_listener_class.return_value.started = False - mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/ring/test_number.py b/tests/components/ring/test_number.py index aa484c6a7b2..9f1581742f2 100644 --- a/tests/components/ring/test_number.py +++ b/tests/components/ring/test_number.py @@ -14,7 +14,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .common import MockConfigEntry, setup_platform +from .common import MockConfigEntry, async_check_entity_translations, setup_platform from tests.common import snapshot_platform @@ -54,6 +54,9 @@ async def test_states( mock_config_entry.add_to_hass(hass) await setup_platform(hass, Platform.NUMBER) + await async_check_entity_translations( + hass, entity_registry, mock_config_entry.entry_id, NUMBER_DOMAIN + ) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 48f679c4524..dcd3d5bddd6 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .common import MockConfigEntry, setup_platform +from .common import MockConfigEntry, async_check_entity_translations, setup_platform from .device_mocks import ( DOWNSTAIRS_DEVICE_ID, FRONT_DEVICE_ID, @@ -57,6 +57,10 @@ def create_deprecated_and_disabled_sensor_entities( create_entry("ingress", "doorbell_volume", INGRESS_DEVICE_ID) create_entry("ingress", "mic_volume", INGRESS_DEVICE_ID) create_entry("ingress", "voice_volume", INGRESS_DEVICE_ID) + for desc in ("last_motion", "last_ding"): + create_entry("front", desc, FRONT_DEVICE_ID) + create_entry("front_door", desc, FRONT_DOOR_DEVICE_ID) + create_entry("internal", desc, INTERNAL_DEVICE_ID) # Disabled for desc in ("wifi_signal_category", "wifi_signal_strength"): @@ -78,6 +82,9 @@ async def test_states( """Test states.""" mock_config_entry.add_to_hass(hass) await setup_platform(hass, Platform.SENSOR) + await async_check_entity_translations( + hass, entity_registry, mock_config_entry.entry_id, SENSOR_DOMAIN + ) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/rituals_perfume_genie/test_init.py b/tests/components/rituals_perfume_genie/test_init.py index 435e762a646..d4d7376a564 100644 --- a/tests/components/rituals_perfume_genie/test_init.py +++ b/tests/components/rituals_perfume_genie/test_init.py @@ -46,6 +46,7 @@ async def test_entity_id_migration( ) -> None: """Test the migration of unique IDs on config entry setup.""" config_entry = mock_config_entry(unique_id="binary_sensor_test_diffuser_v1") + config_entry.add_to_hass(hass) # Pre-create old style unique IDs charging = entity_registry.async_get_or_create( diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 357c644e2fe..9b3a6633c62 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -1,7 +1,12 @@ """Global fixtures for Roborock integration.""" +from collections.abc import Generator from copy import deepcopy -from unittest.mock import patch +import pathlib +import shutil +from typing import Any +from unittest.mock import Mock, patch +import uuid import pytest from roborock import RoborockCategory, RoomMapping @@ -14,9 +19,9 @@ from homeassistant.components.roborock.const import ( CONF_USER_DATA, DOMAIN, ) -from homeassistant.const import CONF_USERNAME +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .mock_data import ( BASE_URL, @@ -25,6 +30,7 @@ from .mock_data import ( MULTI_MAP_LIST, NETWORK_INFO, PROP, + SCENES, USER_DATA, USER_EMAIL, ) @@ -62,15 +68,30 @@ class A01Mock(RoborockMqttClientA01): return {prot: self.protocol_responses[prot] for prot in dyad_data_protocols} +@pytest.fixture(name="bypass_api_client_fixture") +def bypass_api_client_fixture() -> None: + """Skip calls to the API client.""" + with ( + patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + return_value=HOME_DATA, + ), + patch( + "homeassistant.components.roborock.RoborockApiClient.get_scenes", + return_value=SCENES, + ), + ): + yield + + @pytest.fixture(name="bypass_api_fixture") -def bypass_api_fixture() -> None: +def bypass_api_fixture(bypass_api_client_fixture: Any) -> None: """Skip calls to the API.""" with ( patch("homeassistant.components.roborock.RoborockMqttClientV1.async_connect"), patch("homeassistant.components.roborock.RoborockMqttClientV1._send_command"), patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", - return_value=HOME_DATA, + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1._send_command" ), patch( "homeassistant.components.roborock.RoborockMqttClientV1.get_networking", @@ -138,6 +159,22 @@ def bypass_api_fixture() -> None: yield +@pytest.fixture(name="send_message_side_effect") +def send_message_side_effect_fixture() -> Any: + """Fixture to return a side effect for the send_message method.""" + return None + + +@pytest.fixture(name="mock_send_message") +def mock_send_message_fixture(send_message_side_effect: Any) -> Mock: + """Fixture to mock the send_message method.""" + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command", + side_effect=send_message_side_effect, + ) as mock_send_message: + yield mock_send_message + + @pytest.fixture def bypass_api_fixture_v1_only(bypass_api_fixture) -> None: """Bypass api for tests that require only having v1 devices.""" @@ -161,18 +198,48 @@ def mock_roborock_entry(hass: HomeAssistant) -> MockConfigEntry: CONF_USER_DATA: USER_DATA.as_dict(), CONF_BASE_URL: BASE_URL, }, + unique_id=USER_EMAIL, ) mock_entry.add_to_hass(hass) return mock_entry +@pytest.fixture(name="platforms") +def mock_platforms() -> list[Platform]: + """Fixture to specify platforms to test.""" + return [] + + @pytest.fixture async def setup_entry( hass: HomeAssistant, bypass_api_fixture, mock_roborock_entry: MockConfigEntry, -) -> MockConfigEntry: + cleanup_map_storage: pathlib.Path, + platforms: list[Platform], +) -> Generator[MockConfigEntry]: """Set up the Roborock platform.""" - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - return mock_roborock_entry + with patch("homeassistant.components.roborock.PLATFORMS", platforms): + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + await hass.async_block_till_done() + yield mock_roborock_entry + + +@pytest.fixture +async def cleanup_map_storage( + hass: HomeAssistant, mock_roborock_entry: MockConfigEntry +) -> Generator[pathlib.Path]: + """Test cleanup, remove any map storage persisted during the test.""" + tmp_path = str(uuid.uuid4()) + with patch( + "homeassistant.components.roborock.roborock_storage.STORAGE_PATH", new=tmp_path + ): + storage_path = ( + pathlib.Path(hass.config.path(tmp_path)) / mock_roborock_entry.entry_id + ) + yield storage_path + # We need to first unload the config entry because unloading it will + # persist any unsaved maps to storage. + if mock_roborock_entry.state is ConfigEntryState.LOADED: + await hass.config_entries.async_unload(mock_roborock_entry.entry_id) + shutil.rmtree(str(storage_path), ignore_errors=True) diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 6e3fb229aa9..59c54892687 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -9,6 +9,7 @@ from roborock.containers import ( Consumable, DnDTimer, HomeData, + HomeDataScene, MultiMapsList, NetworkInfo, S7Status, @@ -1150,3 +1151,19 @@ MAP_DATA = MapData(0, 0) MAP_DATA.image = ImageData( 100, 10, 10, 10, 10, ImageConfig(), Image.new("RGB", (1, 1)), lambda p: p ) + + +SCENES = [ + HomeDataScene.from_dict( + { + "name": "sc1", + "id": 12, + }, + ), + HomeDataScene.from_dict( + { + "name": "sc2", + "id": 24, + }, + ), +] diff --git a/tests/components/roborock/test_binary_sensor.py b/tests/components/roborock/test_binary_sensor.py index e70dac5ffc9..0e4b338f469 100644 --- a/tests/components/roborock/test_binary_sensor.py +++ b/tests/components/roborock/test_binary_sensor.py @@ -1,10 +1,19 @@ """Test Roborock Binary Sensor.""" +import pytest + +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.BINARY_SENSOR] + + async def test_binary_sensors( hass: HomeAssistant, setup_entry: MockConfigEntry ) -> None: diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py index 43ef043f79c..0a7efe83513 100644 --- a/tests/components/roborock/test_button.py +++ b/tests/components/roborock/test_button.py @@ -6,12 +6,19 @@ import pytest import roborock from homeassistant.components.button import SERVICE_PRESS +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.BUTTON] + + @pytest.mark.parametrize( ("entity_id"), [ diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index 39d8117847c..13bc23e6e2b 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -244,3 +244,28 @@ async def test_reauth_flow( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_roborock_entry.data["user_data"]["rriot"]["s"] == "new_password_hash" + + +async def test_account_already_configured( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Handle the config flow and make sure it succeeds.""" + with patch( + "homeassistant.components.roborock.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.roborock.config_flow.RoborockApiClient.request_code" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USER_EMAIL} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured_account" diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index c884baef123..fd6c8b2796a 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -3,12 +3,17 @@ import copy from datetime import timedelta from http import HTTPStatus +import io from unittest.mock import patch +from PIL import Image +import pytest from roborock import RoborockException +from vacuum_map_parser_base.map_data import ImageConfig, ImageData from homeassistant.components.roborock import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -19,28 +24,39 @@ from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import ClientSessionGenerator +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.IMAGE] + + async def test_floorplan_image( hass: HomeAssistant, setup_entry: MockConfigEntry, hass_client: ClientSessionGenerator, ) -> None: """Test floor plan map image is correctly set up.""" - # Setup calls the image parsing the first time and caches it. assert len(hass.states.async_all("image")) == 4 assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None - # call a second time -should return cached data + # Load the image on demand client = await hass_client() resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") assert resp.status == HTTPStatus.OK body = await resp.read() assert body is not None - # Call a third time - this time forcing it to update - now = dt_util.utcnow() + timedelta(seconds=91) + assert body[0:4] == b"\x89PNG" + + # Call a second time - this time forcing it to update - and save new image + now = dt_util.utcnow() + timedelta(minutes=61) # Copy the device prop so we don't override it prop = copy.deepcopy(PROP) prop.status.in_cleaning = 1 + new_map_data = copy.deepcopy(MAP_DATA) + new_map_data.image = ImageData( + 100, 10, 10, 10, 10, ImageConfig(), Image.new("RGB", (2, 2)), lambda p: p + ) with ( patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", @@ -49,6 +65,10 @@ async def test_floorplan_image( patch( "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now ), + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=new_map_data, + ) as parse_map, ): async_fire_time_changed(hass, now) await hass.async_block_till_done() @@ -56,6 +76,7 @@ async def test_floorplan_image( assert resp.status == HTTPStatus.OK body = await resp.read() assert body is not None + assert parse_map.call_count == 1 async def test_floorplan_image_failed_parse( @@ -90,13 +111,104 @@ async def test_floorplan_image_failed_parse( assert not resp.ok +async def test_load_stored_image( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_entry: MockConfigEntry, +) -> None: + """Test that we correctly load an image from storage when it already exists.""" + img_byte_arr = io.BytesIO() + MAP_DATA.image.data.save(img_byte_arr, format="PNG") + img_bytes = img_byte_arr.getvalue() + + # Load the image on demand, which should queue it to be cached on disk + client = await hass_client() + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + assert resp.status == HTTPStatus.OK + + with patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + ) as parse_map: + # Reload the config entry so that the map is saved in storage and entities exist. + await hass.config_entries.async_reload(setup_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None + client = await hass_client() + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + # Test that we can get the image and it correctly serialized and unserialized. + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == img_bytes + + # Ensure that we never tried to update the map, and only used the cached image. + assert parse_map.call_count == 0 + + +async def test_fail_to_save_image( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we gracefully handle a oserror on saving an image.""" + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + # Ensure that map is still working properly. + assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None + client = await hass_client() + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + # Test that we can get the image and it correctly serialized and unserialized. + assert resp.status == HTTPStatus.OK + + with patch( + "homeassistant.components.roborock.roborock_storage.Path.write_bytes", + side_effect=OSError, + ): + await hass.config_entries.async_unload(mock_roborock_entry.entry_id) + assert "Unable to write map file" in caplog.text + + # Config entry is unloaded successfully + assert mock_roborock_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_fail_to_load_image( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we gracefully handle failing to load an image.""" + with ( + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + ) as parse_map, + patch( + "homeassistant.components.roborock.roborock_storage.Path.exists", + return_value=True, + ), + patch( + "homeassistant.components.roborock.roborock_storage.Path.read_bytes", + side_effect=OSError, + ) as read_bytes, + ): + # Reload the config entry so that the map is saved in storage and entities exist. + await hass.config_entries.async_reload(setup_entry.entry_id) + await hass.async_block_till_done() + assert read_bytes.call_count == 4 + # Ensure that we never updated the map manually since we couldn't load it. + assert parse_map.call_count == 0 + assert "Unable to read map file" in caplog.text + + async def test_fail_parse_on_startup( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_roborock_entry: MockConfigEntry, bypass_api_fixture, ) -> None: - """Test that if we fail parsing on startup, we create the entity but set it as unavailable.""" + """Test that if we fail parsing on startup, we still create the entity.""" map_data = copy.deepcopy(MAP_DATA) map_data.image = None with patch( @@ -108,7 +220,28 @@ async def test_fail_parse_on_startup( assert ( image_entity := hass.states.get("image.roborock_s7_maxv_upstairs") ) is not None - assert image_entity.state == STATE_UNAVAILABLE + assert image_entity.state + + +async def test_fail_get_map_on_startup( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture, +) -> None: + """Test that if we fail getting map on startup, we can still create the entity.""" + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=None, + ), + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert ( + image_entity := hass.states.get("image.roborock_s7_maxv_upstairs") + ) is not None + assert image_entity.state async def test_fail_updating_image( diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index cace9a8ed67..904a3af89d6 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -1,19 +1,28 @@ """Test for Roborock init.""" from copy import deepcopy +from http import HTTPStatus +import pathlib from unittest.mock import patch import pytest -from roborock import RoborockException, RoborockInvalidCredentials +from roborock import ( + RoborockException, + RoborockInvalidCredentials, + RoborockInvalidUserAgreement, + RoborockNoUserAgreement, +) from homeassistant.components.roborock.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .mock_data import HOME_DATA from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator async def test_unload_entry( @@ -128,20 +137,18 @@ async def test_local_client_fails_props( assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY -async def test_fails_maps_continue( +async def test_fail_maps( hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture_v1_only, ) -> None: - """Test that if we fail to get the maps, we still setup.""" + """Test that the integration fails to load if we fail to get the maps.""" with patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", side_effect=RoborockException(), ): await async_setup_component(hass, DOMAIN, {}) - assert mock_roborock_entry.state is ConfigEntryState.LOADED - # No map data means no images - assert len(hass.states.async_all("image")) == 0 + assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY async def test_reauth_started( @@ -160,6 +167,68 @@ async def test_reauth_started( assert flows[0]["step_id"] == "reauth_confirm" +@pytest.mark.parametrize("platforms", [[Platform.IMAGE]]) +async def test_remove_from_hass( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + cleanup_map_storage: pathlib.Path, +) -> None: + """Test that removing from hass removes any existing images.""" + + # Ensure some image content is cached + assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None + client = await hass_client() + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + assert resp.status == HTTPStatus.OK + + assert not cleanup_map_storage.exists() + + # Flush to disk + await hass.config_entries.async_unload(setup_entry.entry_id) + assert cleanup_map_storage.exists() + paths = list(cleanup_map_storage.walk()) + assert len(paths) == 3 # One map image and two directories + + await hass.config_entries.async_remove(setup_entry.entry_id) + # After removal, directories should be empty. + assert not cleanup_map_storage.exists() + + +@pytest.mark.parametrize("platforms", [[Platform.IMAGE]]) +async def test_oserror_remove_image( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + cleanup_map_storage: pathlib.Path, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we gracefully handle failing to remove an image.""" + + # Ensure some image content is cached + assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None + client = await hass_client() + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + assert resp.status == HTTPStatus.OK + + # Image content is saved when unloading + assert not cleanup_map_storage.exists() + await hass.config_entries.async_unload(setup_entry.entry_id) + + assert cleanup_map_storage.exists() + paths = list(cleanup_map_storage.walk()) + assert len(paths) == 3 # One map image and two directories + + with patch( + "homeassistant.components.roborock.roborock_storage.shutil.rmtree", + side_effect=OSError, + ): + await hass.config_entries.async_remove(setup_entry.entry_id) + assert "Unable to remove map files" in caplog.text + + async def test_not_supported_protocol( hass: HomeAssistant, bypass_api_fixture, @@ -194,3 +263,35 @@ async def test_not_supported_a01_device( await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() assert "The device you added is not yet supported" in caplog.text + + +async def test_invalid_user_agreement( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Test that we fail setting up if the user agreement is out of date.""" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + side_effect=RoborockInvalidUserAgreement(), + ): + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + mock_roborock_entry.error_reason_translation_key == "invalid_user_agreement" + ) + + +async def test_no_user_agreement( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Test that we fail setting up if the user has no agreement.""" + with patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + side_effect=RoborockNoUserAgreement(), + ): + await hass.config_entries.async_setup(mock_roborock_entry.entry_id) + assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY + assert mock_roborock_entry.error_reason_translation_key == "no_user_agreement" diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py index 7e87b49253e..bfd8cc6da2b 100644 --- a/tests/components/roborock/test_number.py +++ b/tests/components/roborock/test_number.py @@ -6,12 +6,19 @@ import pytest import roborock from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.NUMBER] + + @pytest.mark.parametrize( ("entity_id", "value"), [ diff --git a/tests/components/roborock/test_scene.py b/tests/components/roborock/test_scene.py new file mode 100644 index 00000000000..15707784feb --- /dev/null +++ b/tests/components/roborock/test_scene.py @@ -0,0 +1,112 @@ +"""Test Roborock Scene platform.""" + +from unittest.mock import ANY, patch + +import pytest +from roborock import RoborockException + +from homeassistant.const import SERVICE_TURN_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import MockConfigEntry + + +@pytest.fixture +def bypass_api_client_get_scenes_fixture(bypass_api_fixture) -> None: + """Fixture to raise when getting scenes.""" + with ( + patch( + "homeassistant.components.roborock.RoborockApiClient.get_scenes", + side_effect=RoborockException(), + ), + ): + yield + + +@pytest.mark.parametrize( + ("entity_id"), + [ + ("scene.roborock_s7_maxv_sc1"), + ("scene.roborock_s7_maxv_sc2"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_get_scenes_failure( + hass: HomeAssistant, + bypass_api_client_get_scenes_fixture, + setup_entry: MockConfigEntry, + entity_id: str, +) -> None: + """Test that if scene retrieval fails, no entity is being created.""" + # Ensure that the entity does not exist + assert hass.states.get(entity_id) is None + + +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SCENE] + + +@pytest.mark.parametrize( + ("entity_id", "scene_id"), + [ + ("scene.roborock_s7_maxv_sc1", 12), + ("scene.roborock_s7_maxv_sc2", 24), + ], +) +@pytest.mark.freeze_time("2023-10-30 08:50:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_execute_success( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + entity_id: str, + scene_id: int, +) -> None: + """Test activating the scene entities.""" + with patch( + "homeassistant.components.roborock.RoborockApiClient.execute_scene" + ) as mock_execute_scene: + await hass.services.async_call( + "scene", + SERVICE_TURN_ON, + blocking=True, + target={"entity_id": entity_id}, + ) + mock_execute_scene.assert_called_once_with(ANY, scene_id) + assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00" + + +@pytest.mark.parametrize( + ("entity_id", "scene_id"), + [ + ("scene.roborock_s7_maxv_sc1", 12), + ], +) +@pytest.mark.freeze_time("2023-10-30 08:50:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_execute_failure( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + entity_id: str, + scene_id: int, +) -> None: + """Test failure while activating the scene entity.""" + with ( + patch( + "homeassistant.components.roborock.RoborockApiClient.execute_scene", + side_effect=RoborockException, + ) as mock_execute_scene, + pytest.raises(HomeAssistantError, match="Error while calling execute_scene"), + ): + await hass.services.async_call( + "scene", + SERVICE_TURN_ON, + blocking=True, + target={"entity_id": entity_id}, + ) + mock_execute_scene.assert_called_once_with(ANY, scene_id) + assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00" diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 784150e24c7..7f25141306b 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -7,16 +7,22 @@ import pytest from roborock.exceptions import RoborockException from homeassistant.components.roborock import DOMAIN -from homeassistant.const import SERVICE_SELECT_OPTION, STATE_UNKNOWN +from homeassistant.const import SERVICE_SELECT_OPTION, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from .mock_data import PROP +from .mock_data import MULTI_MAP_LIST, PROP from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SELECT] + + @pytest.mark.parametrize( ("entity_id", "value"), [ @@ -86,3 +92,33 @@ async def test_none_map_select( await async_setup_component(hass, DOMAIN, {}) select_entity = hass.states.get("select.roborock_s7_maxv_selected_map") assert select_entity.state == STATE_UNKNOWN + + +async def test_selected_map_name( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Test that the selected map is set to the correct map name.""" + await async_setup_component(hass, DOMAIN, {}) + select_entity = hass.states.get("select.roborock_s7_maxv_selected_map") + assert select_entity.state == "Upstairs" + + +async def test_selected_map_without_name( + hass: HomeAssistant, + bypass_api_fixture_v1_only, + mock_roborock_entry: MockConfigEntry, +) -> None: + """Test that maps without a name are given a placeholder name.""" + map_list = copy.deepcopy(MULTI_MAP_LIST) + map_list.map_info[0].name = "" + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_multi_maps_list", + return_value=map_list, + ): + await async_setup_component(hass, DOMAIN, {}) + + select_entity = hass.states.get("select.roborock_s7_maxv_selected_map") + assert select_entity + assert select_entity.state == "Map 0" diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 908754f3b92..e33d3aa78d5 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import patch +import pytest from roborock import DeviceData, HomeDataDevice from roborock.const import ( FILTER_REPLACE_TIME, @@ -12,6 +13,7 @@ from roborock.const import ( from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol from roborock.version_1_apis import RoborockMqttClientV1 +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .mock_data import CONSUMABLE, STATUS, USER_DATA @@ -19,9 +21,15 @@ from .mock_data import CONSUMABLE, STATUS, USER_DATA from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SENSOR] + + async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> None: """Test sensors and check test values are correctly set.""" - assert len(hass.states.async_all("sensor")) == 38 + assert len(hass.states.async_all("sensor")) == 40 assert hass.states.get("sensor.roborock_s7_maxv_main_brush_time_left").state == str( MAIN_BRUSH_REPLACE_TIME - 74382 ) @@ -46,6 +54,7 @@ async def test_sensors(hass: HomeAssistant, setup_entry: MockConfigEntry) -> Non assert hass.states.get("sensor.roborock_s7_maxv_vacuum_error").state == "none" assert hass.states.get("sensor.roborock_s7_maxv_battery").state == "100" assert hass.states.get("sensor.roborock_s7_maxv_dock_error").state == "ok" + assert hass.states.get("sensor.roborock_s7_maxv_total_cleaning_count").state == "31" assert ( hass.states.get("sensor.roborock_s7_maxv_last_clean_begin").state == "2023-01-01T03:22:10+00:00" diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index 5de3c208c1e..e2df9a3498f 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -1,17 +1,24 @@ """Test Roborock Switch platform.""" -from unittest.mock import patch +from unittest.mock import Mock import pytest import roborock from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.SWITCH] + + @pytest.mark.parametrize( ("entity_id"), [ @@ -22,6 +29,7 @@ from tests.common import MockConfigEntry ) async def test_update_success( hass: HomeAssistant, + mock_send_message: Mock, bypass_api_fixture, setup_entry: MockConfigEntry, entity_id: str, @@ -29,27 +37,22 @@ async def test_update_success( """Test turning switch entities on and off.""" # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command" - ) as mock_send_message: - await hass.services.async_call( - "switch", - SERVICE_TURN_ON, - service_data=None, - blocking=True, - target={"entity_id": entity_id}, - ) + await hass.services.async_call( + "switch", + SERVICE_TURN_ON, + service_data=None, + blocking=True, + target={"entity_id": entity_id}, + ) assert mock_send_message.assert_called_once - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_message" - ) as mock_send_message: - await hass.services.async_call( - "switch", - SERVICE_TURN_OFF, - service_data=None, - blocking=True, - target={"entity_id": entity_id}, - ) + mock_send_message.reset_mock() + await hass.services.async_call( + "switch", + SERVICE_TURN_OFF, + service_data=None, + blocking=True, + target={"entity_id": entity_id}, + ) assert mock_send_message.assert_called_once @@ -60,8 +63,12 @@ async def test_update_success( ("switch.roborock_s7_maxv_status_indicator_light", SERVICE_TURN_OFF), ], ) +@pytest.mark.parametrize( + "send_message_side_effect", [roborock.exceptions.RoborockTimeout] +) async def test_update_failed( hass: HomeAssistant, + mock_send_message: Mock, bypass_api_fixture, setup_entry: MockConfigEntry, entity_id: str, @@ -71,10 +78,6 @@ async def test_update_failed( # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None with ( - patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command", - side_effect=roborock.exceptions.RoborockTimeout, - ) as mock_send_message, pytest.raises(HomeAssistantError, match="Failed to update Roborock options"), ): await hass.services.async_call( diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 836a86bd114..9c0a53893ed 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -1,18 +1,25 @@ """Test Roborock Time platform.""" from datetime import time -from unittest.mock import patch +from unittest.mock import Mock import pytest import roborock from homeassistant.components.time import SERVICE_SET_VALUE +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + return [Platform.TIME] + + @pytest.mark.parametrize( ("entity_id"), [ @@ -22,6 +29,7 @@ from tests.common import MockConfigEntry ) async def test_update_success( hass: HomeAssistant, + mock_send_message: Mock, bypass_api_fixture, setup_entry: MockConfigEntry, entity_id: str, @@ -29,16 +37,13 @@ async def test_update_success( """Test turning switch entities on and off.""" # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command" - ) as mock_send_message: - await hass.services.async_call( - "time", - SERVICE_SET_VALUE, - service_data={"time": time(hour=1, minute=1)}, - blocking=True, - target={"entity_id": entity_id}, - ) + await hass.services.async_call( + "time", + SERVICE_SET_VALUE, + service_data={"time": time(hour=1, minute=1)}, + blocking=True, + target={"entity_id": entity_id}, + ) assert mock_send_message.assert_called_once @@ -48,8 +53,12 @@ async def test_update_success( ("time.roborock_s7_maxv_do_not_disturb_begin"), ], ) +@pytest.mark.parametrize( + "send_message_side_effect", [roborock.exceptions.RoborockTimeout] +) async def test_update_failure( hass: HomeAssistant, + mock_send_message: Mock, bypass_api_fixture, setup_entry: MockConfigEntry, entity_id: str, @@ -57,13 +66,7 @@ async def test_update_failure( """Test turning switch entities on and off.""" # Ensure that the entity exist, as these test can pass even if there is no entity. assert hass.states.get(entity_id) is not None - with ( - patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClientV1._send_command", - side_effect=roborock.exceptions.RoborockTimeout, - ) as mock_send_message, - pytest.raises(HomeAssistantError, match="Failed to update Roborock options"), - ): + with pytest.raises(HomeAssistantError, match="Failed to update Roborock options"): await hass.services.async_call( "time", SERVICE_SET_VALUE, diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 5080711d0f9..d9d4340ec83 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -8,9 +8,14 @@ import pytest from roborock import RoborockException from roborock.roborock_typing import RoborockCommand from syrupy.assertion import SnapshotAssertion +from vacuum_map_parser_base.map_data import Point from homeassistant.components.roborock import DOMAIN -from homeassistant.components.roborock.const import GET_MAPS_SERVICE_NAME +from homeassistant.components.roborock.const import ( + GET_MAPS_SERVICE_NAME, + GET_VACUUM_CURRENT_POSITION_SERVICE_NAME, + SET_VACUUM_GOTO_POSITION_SERVICE_NAME, +) from homeassistant.components.vacuum import ( SERVICE_CLEAN_SPOT, SERVICE_LOCATE, @@ -27,7 +32,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from .mock_data import PROP +from .mock_data import MAP_DATA, PROP from tests.common import MockConfigEntry @@ -35,6 +40,15 @@ ENTITY_ID = "vacuum.roborock_s7_maxv" DEVICE_ID = "abc123" +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to set platforms used in the test.""" + # Note: Currently the Image platform is required to make these tests pass since + # some initialization of the coordinator happens as a side effect of loading + # image platform. Fix that and remove IMAGE here. + return [Platform.VACUUM, Platform.IMAGE] + + async def test_registry_entries( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -181,3 +195,112 @@ async def test_get_maps( return_response=True, ) assert response == snapshot + + +async def test_goto( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, +) -> None: + """Test sending the vacuum to specific coordinates.""" + vacuum = hass.states.get(ENTITY_ID) + assert vacuum + + data = {ATTR_ENTITY_ID: ENTITY_ID, "x": 25500, "y": 25500} + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command" + ) as mock_send_command: + await hass.services.async_call( + DOMAIN, + SET_VACUUM_GOTO_POSITION_SERVICE_NAME, + data, + blocking=True, + ) + assert mock_send_command.call_count == 1 + assert mock_send_command.call_args[0][0] == RoborockCommand.APP_GOTO_TARGET + assert mock_send_command.call_args[0][1] == [25500, 25500] + + +async def test_get_current_position( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test that the service for getting the current position outputs the correct coordinates.""" + map_data = copy.deepcopy(MAP_DATA) + map_data.vacuum_position = Point(x=123, y=456) + map_data.image = None + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=b"", + ), + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ), + ): + response = await hass.services.async_call( + DOMAIN, + GET_VACUUM_CURRENT_POSITION_SERVICE_NAME, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + return_response=True, + ) + assert response == { + "vacuum.roborock_s7_maxv": { + "x": 123, + "y": 456, + }, + } + + +async def test_get_current_position_no_map_data( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, +) -> None: + """Test that the service for getting the current position handles no map data error.""" + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=None, + ), + pytest.raises(HomeAssistantError, match="Failed to retrieve map data."), + ): + await hass.services.async_call( + DOMAIN, + GET_VACUUM_CURRENT_POSITION_SERVICE_NAME, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + return_response=True, + ) + + +async def test_get_current_position_no_robot_position( + hass: HomeAssistant, + bypass_api_fixture, + setup_entry: MockConfigEntry, +) -> None: + """Test that the service for getting the current position handles no robot position error.""" + map_data = copy.deepcopy(MAP_DATA) + map_data.vacuum_position = None + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=b"", + ), + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ), + pytest.raises(HomeAssistantError, match="Robot position not found"), + ): + await hass.services.async_call( + DOMAIN, + GET_VACUUM_CURRENT_POSITION_SERVICE_NAME, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + return_response=True, + ) diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index fe3ef215524..3165e5c4ba0 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -2,8 +2,15 @@ from ipaddress import ip_address -from homeassistant.components import ssdp, zeroconf -from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) NAME = "Roku 3" NAME_ROKUTV = '58" Onn Roku TV' @@ -13,7 +20,7 @@ SSDP_LOCATION = "http://192.168.1.160/" UPNP_FRIENDLY_NAME = "My Roku 3" UPNP_SERIAL = "1GU48T017973" -MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( +MOCK_SSDP_DISCOVERY_INFO = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_LOCATION, @@ -25,14 +32,14 @@ MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( HOMEKIT_HOST = "192.168.1.161" -MOCK_HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +MOCK_HOMEKIT_DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=ip_address(HOMEKIT_HOST), ip_addresses=[ip_address(HOMEKIT_HOST)], hostname="mock_hostname", name="onn._hap._tcp.local.", port=None, properties={ - zeroconf.ATTR_PROPERTIES_ID: "2d:97:da:ee:dc:99", + ATTR_PROPERTIES_ID: "2d:97:da:ee:dc:99", }, type="mock_type", ) diff --git a/tests/components/roku/test_select.py b/tests/components/roku/test_select.py index 78cd65250f8..a79a23782ce 100644 --- a/tests/components/roku/test_select.py +++ b/tests/components/roku/test_select.py @@ -22,7 +22,7 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/romy/test_config_flow.py b/tests/components/romy/test_config_flow.py index a29f899ee9d..55d54f3a80b 100644 --- a/tests/components/romy/test_config_flow.py +++ b/tests/components/romy/test_config_flow.py @@ -6,11 +6,14 @@ from unittest.mock import Mock, PropertyMock, patch from romy import RomyRobot from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.romy.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) def _create_mocked_romy( @@ -164,14 +167,14 @@ async def test_show_user_form_robot_reachable_again(hass: HomeAssistant) -> None assert result2["type"] is FlowResultType.CREATE_ENTRY -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=8080, hostname="aicu-aicgsbksisfapcjqmqjq.local", type="mock_type", name="myROMY", - properties={zeroconf.ATTR_PROPERTIES_ID: "aicu-aicgsbksisfapcjqmqjqZERO"}, + properties={ATTR_PROPERTIES_ID: "aicu-aicgsbksisfapcjqmqjqZERO"}, ) diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index dedccc14249..5b6766f7eb9 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from roombapy import RoombaConnectionError, RoombaInfo -from homeassistant.components import dhcp, zeroconf from homeassistant.components.roomba import config_flow from homeassistant.components.roomba.const import ( CONF_BLID, @@ -23,6 +22,8 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -32,7 +33,7 @@ VALID_CONFIG = {CONF_HOST: MOCK_IP, CONF_BLID: "BLID", CONF_PASSWORD: "password" DISCOVERY_DEVICES = [ ( SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=MOCK_IP, macaddress="501479ddeeff", hostname="irobot-blid", @@ -40,7 +41,7 @@ DISCOVERY_DEVICES = [ ), ( SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=MOCK_IP, macaddress="80a589ddeeff", hostname="roomba-blid", @@ -48,7 +49,7 @@ DISCOVERY_DEVICES = [ ), ( SOURCE_ZEROCONF, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(MOCK_IP), ip_addresses=[ip_address(MOCK_IP)], hostname="irobot-blid.local.", @@ -60,7 +61,7 @@ DISCOVERY_DEVICES = [ ), ( SOURCE_ZEROCONF, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(MOCK_IP), ip_addresses=[ip_address(MOCK_IP)], hostname="roomba-blid.local.", @@ -74,12 +75,12 @@ DISCOVERY_DEVICES = [ DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP = [ - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip="4.4.4.4", macaddress="50:14:79:DD:EE:FF", hostname="irobot-blid", ), - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip="5.5.5.5", macaddress="80:A5:89:DD:EE:FF", hostname="roomba-blid", @@ -692,7 +693,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( @pytest.mark.parametrize("discovery_data", DISCOVERY_DEVICES) async def test_dhcp_discovery_and_roomba_discovery_finds( hass: HomeAssistant, - discovery_data: tuple[str, dhcp.DhcpServiceInfo | zeroconf.ZeroconfServiceInfo], + discovery_data: tuple[str, DhcpServiceInfo | ZeroconfServiceInfo], ) -> None: """Test we can process the discovery from dhcp and roomba discovery matches the device.""" @@ -910,7 +911,7 @@ async def test_dhcp_discovery_with_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-blid", @@ -933,7 +934,7 @@ async def test_dhcp_discovery_already_configured_host(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-blid", @@ -959,7 +960,7 @@ async def test_dhcp_discovery_already_configured_blid(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-blid", @@ -985,7 +986,7 @@ async def test_dhcp_discovery_not_irobot(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="Notirobot-blid", @@ -1006,7 +1007,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-blid", @@ -1023,7 +1024,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-blidthatislonger", @@ -1044,7 +1045,7 @@ async def test_dhcp_discovery_partial_hostname(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-bl", @@ -1082,7 +1083,7 @@ async def test_dhcp_discovery_when_user_flow_in_progress(hass: HomeAssistant) -> result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=MOCK_IP, macaddress="aabbccddeeff", hostname="irobot-blidthatislonger", diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr index 5e607e6a8df..8eb77006061 100644 --- a/tests/components/rova/snapshots/test_init.ambr +++ b/tests/components/rova/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/rova/snapshots/test_sensor.ambr b/tests/components/rova/snapshots/test_sensor.ambr index 866f1c735c1..90cf29a1b89 100644 --- a/tests/components/rova/snapshots/test_sensor.ambr +++ b/tests/components/rova/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 5522c1e6ea2..2516bd81650 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -9,9 +9,10 @@ from aiorussound.util import controller_device_str, zone_device_str import pytest from homeassistant.components.russound_rio.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from .const import API_VERSION, HARDWARE_MAC, HOST, MOCK_CONFIG, MODEL, PORT +from .const import API_VERSION, HARDWARE_MAC, MOCK_CONFIG, MODEL from tests.common import MockConfigEntry, load_json_object_fixture @@ -46,29 +47,62 @@ def mock_russound_client() -> Generator[AsyncMock]: ), ): client = mock_client.return_value - zones = { - int(k): ZoneControlSurface.from_dict(v) - for k, v in load_json_object_fixture("get_zones.json", DOMAIN).items() + controller_zones = { + int(controller_id): { + int(zone_id): ZoneControlSurface.from_dict(zone) + for zone_id, zone in v["zones"].items() + } + for controller_id, v in load_json_object_fixture("get_zones.json", DOMAIN)[ + "controllers" + ].items() } client.sources = { int(k): Source.from_dict(v) for k, v in load_json_object_fixture("get_sources.json", DOMAIN).items() } client.state = load_json_object_fixture("get_state.json", DOMAIN) - for k, v in zones.items(): - v.device_str = zone_device_str(1, k) - v.fetch_current_source = Mock( - side_effect=lambda current_source=v.current_source: client.sources.get( - int(current_source) + for controller_id, zones in controller_zones.items(): + for zone_id, zone in zones.items(): + zone.device_str = zone_device_str(controller_id, zone_id) + zone.fetch_current_source = Mock( + side_effect=lambda current_source=zone.current_source: client.sources.get( + int(current_source) + ) ) - ) + zone.volume_up = AsyncMock() + zone.volume_down = AsyncMock() + zone.set_volume = AsyncMock() + zone.zone_on = AsyncMock() + zone.zone_off = AsyncMock() + zone.select_source = AsyncMock() + zone.mute = AsyncMock() + zone.unmute = AsyncMock() + zone.toggle_mute = AsyncMock() + zone.set_seek_time = AsyncMock() client.controllers = { 1: Controller( - 1, "MCA-C5", client, controller_device_str(1), HARDWARE_MAC, None, zones - ) + 1, + MODEL, + client, + controller_device_str(1), + HARDWARE_MAC, + None, + controller_zones[1], + ), + 2: Controller( + 2, + MODEL, + client, + controller_device_str(2), + None, + None, + controller_zones[2], + ), } - client.connection_handler = RussoundTcpConnectionHandler(HOST, PORT) + client.connection_handler = RussoundTcpConnectionHandler( + MOCK_CONFIG[CONF_HOST], MOCK_CONFIG[CONF_PORT] + ) client.is_connected = Mock(return_value=True) client.unregister_state_update_callbacks.return_value = True client.rio_version = API_VERSION diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 8f8ae7b59ea..8269e825e33 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -1,22 +1,21 @@ """Constants for russound_rio tests.""" -from collections import namedtuple - from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT -HOST = "127.0.0.1" -PORT = 9621 MODEL = "MCA-C5" HARDWARE_MAC = "00:11:22:33:44:55" API_VERSION = "1.08.00" MOCK_CONFIG = { - "host": HOST, - "port": PORT, + CONF_HOST: "192.168.20.75", + CONF_PORT: 9621, } -_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) # noqa: PYI024 -MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)} +MOCK_RECONFIGURATION_CONFIG = { + CONF_HOST: "192.168.20.70", + CONF_PORT: 9622, +} DEVICE_NAME = "mca_c5" NAME_ZONE_1 = "backyard" diff --git a/tests/components/russound_rio/fixtures/get_zones.json b/tests/components/russound_rio/fixtures/get_zones.json index 396310339b3..e1077944593 100644 --- a/tests/components/russound_rio/fixtures/get_zones.json +++ b/tests/components/russound_rio/fixtures/get_zones.json @@ -1,22 +1,38 @@ { - "1": { - "name": "Backyard", - "volume": "10", - "status": "ON", - "enabled": "True", - "current_source": "1" - }, - "2": { - "name": "Kitchen", - "volume": "50", - "status": "OFF", - "enabled": "True", - "current_source": "2" - }, - "3": { - "name": "Bedroom", - "volume": "10", - "status": "OFF", - "enabled": "False" + "controllers": { + "1": { + "zones": { + "1": { + "name": "Backyard", + "volume": "10", + "status": "ON", + "enabled": "True", + "current_source": "1" + }, + "2": { + "name": "Kitchen", + "volume": "50", + "status": "OFF", + "enabled": "True", + "current_source": "2" + }, + "3": { + "name": "Bedroom", + "volume": "10", + "status": "OFF", + "enabled": "False" + } + } + }, + "2": { + "zones": { + "9": { + "name": "Living Room", + "volume": "10", + "status": "OFF", + "enabled": "True" + } + } + } } } diff --git a/tests/components/russound_rio/snapshots/test_init.ambr b/tests/components/russound_rio/snapshots/test_init.ambr index fcd59dd06f7..e3185a06b24 100644 --- a/tests/components/russound_rio/snapshots/test_init.ambr +++ b/tests/components/russound_rio/snapshots/test_init.ambr @@ -3,7 +3,8 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , - 'configuration_url': 'http://127.0.0.1', + 'config_entries_subentries': , + 'configuration_url': 'http://192.168.20.75', 'connections': set({ tuple( 'mac', diff --git a/tests/components/russound_rio/test_config_flow.py b/tests/components/russound_rio/test_config_flow.py index 28cbf7eda5e..550ea9404df 100644 --- a/tests/components/russound_rio/test_config_flow.py +++ b/tests/components/russound_rio/test_config_flow.py @@ -1,16 +1,36 @@ """Test the Russound RIO config flow.""" +from ipaddress import ip_address from unittest.mock import AsyncMock from homeassistant.components.russound_rio.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .const import MOCK_CONFIG, MODEL +from .const import MOCK_CONFIG, MOCK_RECONFIGURATION_CONFIG, MODEL from tests.common import MockConfigEntry +ZEROCONF_DISCOVERY = ZeroconfServiceInfo( + ip_address=ip_address("192.168.20.17"), + ip_addresses=[ip_address("192.168.20.17")], + hostname="controller1.local.", + name="controller1._stream-magic._tcp.local.", + port=9621, + type="_rio._tcp.local.", + properties={ + "txtvers": "0", + "productType": "2", + "productId": "59", + "version": "07.04.00", + "buildDate": "Jul 8 2019", + "localName": "0", + }, +) + async def test_form( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock @@ -88,32 +108,214 @@ async def test_duplicate( assert result["reason"] == "already_configured" -async def test_import( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound_client: AsyncMock +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, ) -> None: - """Test we import a config entry.""" + """Test zeroconf flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_IMPORT}, - data=MOCK_CONFIG, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == MODEL - assert result["data"] == MOCK_CONFIG - assert len(mock_setup_entry.mock_calls) == 1 + assert result["title"] == "MCA-C5" + assert result["data"] == { + CONF_HOST: "192.168.20.17", + CONF_PORT: 9621, + } assert result["result"].unique_id == "00:11:22:33:44:55" -async def test_import_cannot_connect( - hass: HomeAssistant, mock_russound_client: AsyncMock +async def test_zeroconf_flow_errors( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, ) -> None: - """Test we handle import cannot connect error.""" + """Test zeroconf flow.""" mock_russound_client.connect.side_effect = TimeoutError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + mock_russound_client.connect.side_effect = None result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "MCA-C5" + assert result["data"] == { + CONF_HOST: "192.168.20.17", + CONF_PORT: 9621, + } + assert result["result"].unique_id == "00:11:22:33:44:55" + + +async def test_zeroconf_duplicate( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_duplicate_different_ip( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow with different IP.""" + mock_config_entry.add_to_hass(hass) + + ZEROCONF_DISCOVERY_DIFFERENT_IP = ZeroconfServiceInfo( + ip_address=ip_address("192.168.20.18"), + ip_addresses=[ip_address("192.168.20.18")], + hostname="controller1.local.", + name="controller1._stream-magic._tcp.local.", + port=9621, + type="_rio._tcp.local.", + properties={ + "txtvers": "0", + "productType": "2", + "productId": "59", + "version": "07.04.00", + "buildDate": "Jul 8 2019", + "localName": "0", + }, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY_DIFFERENT_IP, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry + assert entry.data == { + CONF_HOST: "192.168.20.18", + CONF_PORT: 9621, + } + + +async def test_user_flow_works_discovery( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test user flow can continue after discovery happened.""" + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def _start_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> ConfigFlowResult: + """Initialize a reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + + return reconfigure_result + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + + reconfigure_result = await _start_reconfigure_flow(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + MOCK_RECONFIGURATION_CONFIG, ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + assert result["reason"] == "reconfigure_successful" + + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry + assert entry.data == { + CONF_HOST: "192.168.20.70", + CONF_PORT: 9622, + } + + +async def test_reconfigure_unique_id_mismatch( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure reconfigure flow aborts when the bride changes.""" + mock_russound_client.controllers[1].mac_address = "different_mac" + + reconfigure_result = await _start_reconfigure_flow(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], + MOCK_RECONFIGURATION_CONFIG, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device" diff --git a/tests/components/russound_rio/test_init.py b/tests/components/russound_rio/test_init.py index e7022fa6ac1..d654eea32bd 100644 --- a/tests/components/russound_rio/test_init.py +++ b/tests/components/russound_rio/test_init.py @@ -59,8 +59,8 @@ async def test_disconnect_reconnect_log( mock_russound_client.is_connected = Mock(return_value=False) await mock_state_update(mock_russound_client, CallbackType.CONNECTION) - assert "Disconnected from device at 127.0.0.1" in caplog.text + assert "Disconnected from device at 192.168.20.75" in caplog.text mock_russound_client.is_connected = Mock(return_value=True) await mock_state_update(mock_russound_client, CallbackType.CONNECTION) - assert "Reconnected to device at 127.0.0.1" in caplog.text + assert "Reconnected to device at 192.168.20.75" in caplog.text diff --git a/tests/components/russound_rio/test_media_player.py b/tests/components/russound_rio/test_media_player.py index c740ec4f39e..d0c18a9b1e7 100644 --- a/tests/components/russound_rio/test_media_player.py +++ b/tests/components/russound_rio/test_media_player.py @@ -2,10 +2,28 @@ from unittest.mock import AsyncMock +from aiorussound.const import FeatureFlag +from aiorussound.exceptions import CommandError from aiorussound.models import PlayStatus import pytest +from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN as MP_DOMAIN, + SERVICE_SELECT_SOURCE, +) from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_MEDIA_SEEK, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_BUFFERING, STATE_IDLE, STATE_OFF, @@ -14,6 +32,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import mock_state_update, setup_integration from .const import ENTITY_ID_ZONE_1 @@ -50,3 +69,187 @@ async def test_entity_state( state = hass.states.get(ENTITY_ID_ZONE_1) assert state.state == media_player_state + + +async def test_media_volume( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test volume service.""" + await setup_integration(hass, mock_config_entry) + + # Test volume up + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_UP, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].volume_up.assert_called_once() + + # Test volume down + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_DOWN, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].volume_down.assert_called_once() + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_LEVEL: 0.30}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].set_volume.assert_called_once_with( + "15" + ) + + +async def test_volume_mute( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test mute service.""" + await setup_integration(hass, mock_config_entry) + + # Test mute (w/ toggle mute support) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].toggle_mute.assert_called_once() + mock_russound_client.controllers[1].zones[1].toggle_mute.reset_mock() + + mock_russound_client.controllers[1].zones[1].is_mute = True + + # Test mute when already muted (w/ toggle mute support) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].toggle_mute.assert_not_called() + mock_russound_client.supported_features = [FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON] + + # Test mute (w/ dedicated commands) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: True}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].mute.assert_called_once() + + # Test unmute (w/ dedicated commands) + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_VOLUME_MUTED: False}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].unmute.assert_called_once() + + +@pytest.mark.parametrize( + ("source_name", "source_id"), + [ + ("Aux", 1), + ("Spotify", 2), + ], +) +async def test_source_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, + source_name: str, + source_id: int, +) -> None: + """Test source service.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_INPUT_SOURCE: source_name}, + blocking=True, + ) + + mock_russound_client.controllers[1].zones[1].select_source.assert_called_once_with( + source_id + ) + + +async def test_invalid_source_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test source service with invalid source ID.""" + await setup_integration(hass, mock_config_entry) + + mock_russound_client.controllers[1].zones[ + 1 + ].select_source.side_effect = CommandError + + with pytest.raises( + HomeAssistantError, + match="Error executing async_select_source on entity media_player.mca_c5_backyard", + ): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_INPUT_SOURCE: "Aux"}, + blocking=True, + ) + + +async def test_power_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test power service.""" + await setup_integration(hass, mock_config_entry) + + data = {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1} + + await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + mock_russound_client.controllers[1].zones[1].zone_on.assert_called_once() + + await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + + mock_russound_client.controllers[1].zones[1].zone_off.assert_called_once() + + +async def test_media_seek( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_russound_client: AsyncMock, +) -> None: + """Test media seek service.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_MEDIA_SEEK, + {ATTR_ENTITY_ID: ENTITY_ID_ZONE_1, ATTR_MEDIA_SEEK_POSITION: 100}, + ) + + mock_russound_client.controllers[1].zones[1].set_seek_time.assert_called_once_with( + 100 + ) diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index c4ecf929f94..14f74a7add7 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -6,10 +6,10 @@ from aioruuvigateway.excs import CannotConnect, InvalidAuth import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.ruuvi_gateway.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .consts import ( BASE_DATA, @@ -32,7 +32,7 @@ DHCP_DATA = {**BASE_DATA, "host": DHCP_IP} BASE_DATA, ), ( - dhcp.DhcpServiceInfo( + DhcpServiceInfo( hostname="RuuviGateway1234", ip=DHCP_IP, macaddress="1234567890ab", diff --git a/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr b/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr index 9f3087df3d1..1feaece1c3e 100644 --- a/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr +++ b/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sabnzbd/snapshots/test_button.ambr b/tests/components/sabnzbd/snapshots/test_button.ambr index 9b965e10518..f09bb44e8e4 100644 --- a/tests/components/sabnzbd/snapshots/test_button.ambr +++ b/tests/components/sabnzbd/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sabnzbd/snapshots/test_number.ambr b/tests/components/sabnzbd/snapshots/test_number.ambr index 6a370797264..623002470b7 100644 --- a/tests/components/sabnzbd/snapshots/test_number.ambr +++ b/tests/components/sabnzbd/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sabnzbd/snapshots/test_sensor.ambr b/tests/components/sabnzbd/snapshots/test_sensor.ambr index 8b977e69aa6..893d270a569 100644 --- a/tests/components/sabnzbd/snapshots/test_sensor.ambr +++ b/tests/components/sabnzbd/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -62,6 +63,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -113,6 +115,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -164,6 +167,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -218,6 +222,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -272,6 +277,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -323,6 +329,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -375,6 +382,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -430,6 +438,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -478,6 +487,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -529,6 +539,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index ec12031ef96..105ef0f25ad 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -21,7 +21,7 @@ from samsungtvws.exceptions import ResponseError from samsungtvws.remote import ChannelEmitCommand from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .const import SAMPLE_DEVICE_INFO_UE48JU6400, SAMPLE_DEVICE_INFO_WIFI diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 1a7347ff0ce..c1a9da4e284 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -2,18 +2,11 @@ from samsungtvws.event import ED_INSTALLED_APP_EVENT -from homeassistant.components import ssdp from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, METHOD_LEGACY, METHOD_WEBSOCKET, ) -from homeassistant.components.ssdp import ( - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME, - ATTR_UPNP_UDN, -) from homeassistant.const import ( CONF_HOST, CONF_IP_ADDRESS, @@ -24,6 +17,13 @@ from homeassistant.const import ( CONF_PORT, CONF_TOKEN, ) +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) MOCK_CONFIG = { CONF_HOST: "fake_host", @@ -61,7 +61,7 @@ MOCK_ENTRY_WS_WITH_MAC = { CONF_TOKEN: "123456789", } -MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo( +MOCK_SSDP_DATA_RENDERING_CONTROL_ST = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1", ssdp_location="https://fake_host:12345/test", @@ -72,7 +72,7 @@ MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", }, ) -MOCK_SSDP_DATA_MAIN_TV_AGENT_ST = ssdp.SsdpServiceInfo( +MOCK_SSDP_DATA_MAIN_TV_AGENT_ST = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="urn:samsung.com:service:MainTVAgent2:1", ssdp_location="https://fake_host:12345/tv_agent", diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 017a2bc3e60..ad01b5454ff 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -46,6 +47,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -115,6 +117,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index eb78332b7b3..576a5f6d534 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -17,7 +17,6 @@ from websockets import frames from websockets.exceptions import ConnectionClosedError, WebSocketException from homeassistant import config_entries -from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.samsungtv.config_flow import SamsungTVConfigFlow from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, @@ -33,13 +32,6 @@ from homeassistant.components.samsungtv.const import ( TIMEOUT_REQUEST, TIMEOUT_WEBSOCKET, ) -from homeassistant.components.ssdp import ( - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME, - ATTR_UPNP_UDN, - SsdpServiceInfo, -) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, @@ -54,6 +46,15 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import BaseServiceInfo, FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component from .const import ( @@ -83,7 +84,7 @@ MOCK_IMPORT_WSDATA = { CONF_PORT: 8002, } MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} -MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( +MOCK_SSDP_DATA = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="https://fake_host:12345/test", @@ -94,7 +95,7 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", }, ) -MOCK_SSDP_DATA_NO_MANUFACTURER = ssdp.SsdpServiceInfo( +MOCK_SSDP_DATA_NO_MANUFACTURER = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="https://fake_host:12345/test", @@ -104,7 +105,7 @@ MOCK_SSDP_DATA_NO_MANUFACTURER = ssdp.SsdpServiceInfo( }, ) -MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( +MOCK_SSDP_DATA_NOPREFIX = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://fake2_host:12345/test", @@ -115,7 +116,7 @@ MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", }, ) -MOCK_SSDP_DATA_WRONGMODEL = ssdp.SsdpServiceInfo( +MOCK_SSDP_DATA_WRONGMODEL = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://fake2_host:12345/test", @@ -126,11 +127,11 @@ MOCK_SSDP_DATA_WRONGMODEL = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", }, ) -MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( +MOCK_DHCP_DATA = DhcpServiceInfo( ip="fake_host", macaddress="aabbccddeeff", hostname="fake_hostname" ) EXISTING_IP = "192.168.40.221" -MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="mock_hostname", @@ -1704,7 +1705,7 @@ async def test_update_legacy_missing_mac_from_dhcp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=EXISTING_IP, macaddress="aabbccddeeff", hostname="fake_hostname" ), ) @@ -1741,7 +1742,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=EXISTING_IP, macaddress="aabbccddeeff", hostname="fake_hostname" ), ) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1a7c8713b17..3d9633bbf96 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -78,7 +78,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceNotSupported from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import async_wait_config_entry_reload, setup_samsungtv_entry from .const import ( diff --git a/tests/components/sanix/snapshots/test_sensor.ambr b/tests/components/sanix/snapshots/test_sensor.ambr index 84c97ce68b1..6cf0254b66b 100644 --- a/tests/components/sanix/snapshots/test_sensor.ambr +++ b/tests/components/sanix/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -57,6 +58,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -105,6 +107,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -156,6 +159,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -204,6 +208,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -251,6 +256,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/schedule/snapshots/test_init.ambr b/tests/components/schedule/snapshots/test_init.ambr new file mode 100644 index 00000000000..93cde4f5733 --- /dev/null +++ b/tests/components/schedule/snapshots/test_init.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_service_get[schedule.from_storage-get-after-update] + dict({ + 'friday': list([ + ]), + 'monday': list([ + ]), + 'saturday': list([ + ]), + 'sunday': list([ + ]), + 'thursday': list([ + ]), + 'tuesday': list([ + ]), + 'wednesday': list([ + dict({ + 'from': datetime.time(17, 0), + 'to': datetime.time(19, 0), + }), + ]), + }) +# --- +# name: test_service_get[schedule.from_storage-get] + dict({ + 'friday': list([ + dict({ + 'data': dict({ + 'party_level': 'epic', + }), + 'from': datetime.time(17, 0), + 'to': datetime.time(23, 59, 59), + }), + ]), + 'monday': list([ + ]), + 'saturday': list([ + dict({ + 'from': datetime.time(0, 0), + 'to': datetime.time(23, 59, 59), + }), + ]), + 'sunday': list([ + dict({ + 'data': dict({ + 'entry': 'VIPs only', + }), + 'from': datetime.time(0, 0), + 'to': datetime.time(23, 59, 59, 999999), + }), + ]), + 'thursday': list([ + ]), + 'tuesday': list([ + ]), + 'wednesday': list([ + ]), + }) +# --- diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index 18346122bfd..fef2ff745cd 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -8,10 +8,12 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR from homeassistant.components.schedule.const import ( ATTR_NEXT_EVENT, + CONF_ALL_DAYS, CONF_DATA, CONF_FRIDAY, CONF_FROM, @@ -23,12 +25,14 @@ from homeassistant.components.schedule.const import ( CONF_TUESDAY, CONF_WEDNESDAY, DOMAIN, + SERVICE_GET, ) from homeassistant.const import ( ATTR_EDITABLE, ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_NAME, + CONF_ENTITY_ID, CONF_ICON, CONF_ID, CONF_NAME, @@ -754,3 +758,66 @@ async def test_ws_create( assert result["party_mode"][CONF_MONDAY] == [ {CONF_FROM: "12:00:00", CONF_TO: saved_to} ] + + +async def test_service_get( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test getting a single schedule via service.""" + assert await schedule_setup() + + entity_id = "schedule.from_storage" + + # Test retrieving a single schedule via service call + service_result = await hass.services.async_call( + DOMAIN, + SERVICE_GET, + { + CONF_ENTITY_ID: entity_id, + }, + blocking=True, + return_response=True, + ) + result = service_result.get(entity_id) + + assert set(result) == CONF_ALL_DAYS + assert result == snapshot(name=f"{entity_id}-get") + + # Now we update the schedule via WS + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": entity_id.rsplit(".", maxsplit=1)[-1], + CONF_NAME: "Party pooper", + CONF_ICON: "mdi:party-pooper", + CONF_MONDAY: [], + CONF_TUESDAY: [], + CONF_WEDNESDAY: [{CONF_FROM: "17:00:00", CONF_TO: "19:00:00"}], + CONF_THURSDAY: [], + CONF_FRIDAY: [], + CONF_SATURDAY: [], + CONF_SUNDAY: [], + } + ) + resp = await client.receive_json() + assert resp["success"] + + # Test retrieving the schedule via service call after WS update + service_result = await hass.services.async_call( + DOMAIN, + SERVICE_GET, + { + CONF_ENTITY_ID: entity_id, + }, + blocking=True, + return_response=True, + ) + result = service_result.get(entity_id) + + assert set(result) == CONF_ALL_DAYS + assert result == snapshot(name=f"{entity_id}-get-after-update") diff --git a/tests/components/schlage/snapshots/test_init.ambr b/tests/components/schlage/snapshots/test_init.ambr index c7049443ab7..a7f94b80038 100644 --- a/tests/components/schlage/snapshots/test_init.ambr +++ b/tests/components/schlage/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/schlage/test_init.py b/tests/components/schlage/test_init.py index 57a139e582e..97da66c7e93 100644 --- a/tests/components/schlage/test_init.py +++ b/tests/components/schlage/test_init.py @@ -12,7 +12,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.schlage.const import DOMAIN, UPDATE_INTERVAL from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -import homeassistant.helpers.device_registry as dr +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceRegistry from . import MockSchlageConfigEntry diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index d1f2a22d036..dc9bf912c2d 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -443,9 +443,9 @@ async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") - assert state.state == STATE_UNKNOWN + assert state.state == STATE_UNAVAILABLE state2 = hass.states.get("sensor.ha_class2") - assert state2.state == STATE_UNKNOWN + assert state2.state == STATE_UNAVAILABLE async def test_scrape_sensor_unique_id( diff --git a/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json b/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json index aa0df6e3df6..04437c12ebf 100644 --- a/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json +++ b/tests/components/screenlogic/fixtures/data_missing_values_chem_chlor.json @@ -619,7 +619,7 @@ }, "heat_mode": { "name": "Spa Heat Mode", - "value": 0, + "value": 3, "device_type": "enum", "enum_options": [ "Off", diff --git a/tests/components/screenlogic/test_climate.py b/tests/components/screenlogic/test_climate.py new file mode 100644 index 00000000000..c2e96a35508 --- /dev/null +++ b/tests/components/screenlogic/test_climate.py @@ -0,0 +1,103 @@ +"""Tests for ScreenLogic climate entity.""" + +import logging +from unittest.mock import DEFAULT, patch + +import pytest +from screenlogicpy import ScreenLogicGateway +from screenlogicpy.device_const.heat import HEAT_MODE + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODES, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + ATTR_TEMPERATURE, + DOMAIN as CLIMATE_DOMAIN, + HVACAction, + HVACMode, +) +from homeassistant.core import HomeAssistant +from homeassistant.util import slugify + +from . import ( + DATA_MISSING_VALUES_CHEM_CHLOR, + GATEWAY_DISCOVERY_IMPORT_PATH, + MOCK_ADAPTER_NAME, + stub_async_connect, +) + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +@pytest.mark.parametrize( + ( + "tested_dataset", + "expected_entity_states", + ), + [ + ( + DATA_MISSING_VALUES_CHEM_CHLOR, + { + f"{CLIMATE_DOMAIN}.{slugify(MOCK_ADAPTER_NAME)}_pool_heat": { + "state": HVACMode.OFF, + "attributes": { + ATTR_CURRENT_TEMPERATURE: 27.2, + ATTR_TEMPERATURE: 28.3, + ATTR_HVAC_ACTION: HVACAction.OFF, + ATTR_HVAC_MODES: [HVACMode.OFF, HVACMode.HEAT], + ATTR_PRESET_MODE: "heater", + ATTR_PRESET_MODES: [HEAT_MODE.HEATER.name.lower()], + }, + }, + f"{CLIMATE_DOMAIN}.{slugify(MOCK_ADAPTER_NAME)}_spa_heat": { + "state": HVACMode.HEAT, + "attributes": { + ATTR_CURRENT_TEMPERATURE: 28.9, + ATTR_TEMPERATURE: 34.4, + ATTR_HVAC_ACTION: HVACAction.IDLE, + ATTR_HVAC_MODES: [HVACMode.OFF, HVACMode.HEAT], + ATTR_PRESET_MODE: "heater", + ATTR_PRESET_MODES: [HEAT_MODE.HEATER.name.lower()], + }, + }, + }, + ) + ], +) +async def test_climate_state( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + tested_dataset: dict, + expected_entity_states: dict, +) -> None: + """Test setup for platforms that define expected data.""" + + def stub_connect(*args, **kwargs): + return stub_async_connect(tested_dataset, *args, **kwargs) + + mock_config_entry.add_to_hass(hass) + + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=stub_connect, + is_connected=True, + _async_connected_request=DEFAULT, + ), + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + for entity_id, state_data in expected_entity_states.items(): + assert (climate_state := hass.states.get(entity_id)) is not None + assert climate_state.state == state_data["state"] + for attribute, value in state_data["attributes"].items(): + assert climate_state.attributes[attribute] == value diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index 8ca6bd4cb90..ad8ef125dac 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -12,7 +12,6 @@ from screenlogicpy.const.common import ( ) from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.screenlogic.config_flow import ( GATEWAY_MANUAL_ENTRY, GATEWAY_SELECT_KEY, @@ -25,6 +24,7 @@ from homeassistant.components.screenlogic.const import ( from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -86,6 +86,53 @@ async def test_flow_discover_none(hass: HomeAssistant) -> None: assert result["step_id"] == "gateway_entry" +async def test_flow_replace_ignored(hass: HomeAssistant) -> None: + """Test we can replace ignored entries.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:c0:33:01:01:01", + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.screenlogic.config_flow.discovery.async_discover", + return_value=[ + { + SL_GATEWAY_IP: "1.1.1.1", + SL_GATEWAY_PORT: 80, + SL_GATEWAY_TYPE: 12, + SL_GATEWAY_SUBTYPE: 2, + SL_GATEWAY_NAME: "Pentair: 01-01-01", + }, + ], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "gateway_select" + + with patch( + "homeassistant.components.screenlogic.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={GATEWAY_SELECT_KEY: "00:c0:33:01:01:01"} + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Pentair: 01-01-01" + assert result2["data"] == { + CONF_IP_ADDRESS: "1.1.1.1", + CONF_PORT: 80, + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_flow_discover_error(hass: HomeAssistant) -> None: """Test when discovery errors.""" @@ -135,7 +182,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="Pentair: 01-01-01", ip="1.1.1.1", macaddress="aabbccddeeff", diff --git a/tests/components/screenlogic/test_init.py b/tests/components/screenlogic/test_init.py index 6416c93f779..b177f860606 100644 --- a/tests/components/screenlogic/test_init.py +++ b/tests/components/screenlogic/test_init.py @@ -4,12 +4,14 @@ from dataclasses import dataclass from unittest.mock import DEFAULT, patch import pytest -from screenlogicpy import ScreenLogicGateway +from screenlogicpy import ScreenLogicError, ScreenLogicGateway +from screenlogicpy.const.common import ScreenLogicConnectionError from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.screenlogic import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import slugify @@ -41,7 +43,7 @@ TEST_MIGRATING_ENTITIES = [ EntityMigrationData( "Chemistry Alarm", "chem_alarm", - "Active Alert", + "Active alert", "active_alert", BINARY_SENSOR_DOMAIN, ), @@ -284,3 +286,35 @@ async def test_platform_setup( for entity_id in tested_entity_ids: assert hass.states.get(entity_id) is not None + + +@pytest.mark.parametrize( + "exception", + [ScreenLogicConnectionError, ScreenLogicError], +) +async def test_retry_on_connect_exception( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, exception: Exception +) -> None: + """Test setup retries on expected exceptions.""" + + def stub_connect(*args, **kwargs): + raise exception + + mock_config_entry.add_to_hass(hass) + + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=stub_connect, + is_connected=False, + _async_connected_request=DEFAULT, + ), + ): + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index 7f03a89c548..f65e5483ae4 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -18,7 +18,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, template from homeassistant.setup import async_setup_component -from homeassistant.util import yaml +from homeassistant.util import yaml as yaml_util from tests.common import MockConfigEntry, async_mock_service @@ -37,7 +37,7 @@ def patch_blueprint(blueprint_path: str, data_path: str) -> Iterator[None]: return orig_load(self, path) return Blueprint( - yaml.load_yaml(data_path), + yaml_util.load_yaml(data_path), expected_domain=self.domain, path=path, schema=BLUEPRINT_SCHEMA, diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index a5eda3757a9..3b0bff7e82e 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -42,8 +42,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 homeassistant.util import dt as dt_util, yaml as yaml_util from tests.common import ( MockConfigEntry, @@ -1722,7 +1721,7 @@ async def test_blueprint_script_fails_substitution( """Test blueprint script with bad inputs.""" with patch( "homeassistant.components.blueprint.models.BlueprintInputs.async_substitute", - side_effect=yaml.UndefinedSubstitution("blah"), + side_effect=yaml_util.UndefinedSubstitution("blah"), ): assert await async_setup_component( hass, diff --git a/tests/components/sense/snapshots/test_binary_sensor.ambr b/tests/components/sense/snapshots/test_binary_sensor.ambr index 339830b16d3..7221a0bc518 100644 --- a/tests/components/sense/snapshots/test_binary_sensor.ambr +++ b/tests/components/sense/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,6 +56,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sense/snapshots/test_sensor.ambr b/tests/components/sense/snapshots/test_sensor.ambr index 4a3507880a1..0a68553cf04 100644 --- a/tests/components/sense/snapshots/test_sensor.ambr +++ b/tests/components/sense/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -64,6 +65,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -120,6 +122,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -176,6 +179,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -229,6 +233,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -285,6 +290,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -341,6 +347,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -397,6 +404,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -453,6 +461,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -509,6 +518,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -562,6 +572,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -618,6 +629,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -674,6 +686,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -727,6 +740,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -780,6 +794,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -831,6 +846,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -881,6 +897,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -932,6 +949,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -982,6 +1000,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1035,6 +1054,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1088,6 +1108,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1141,6 +1162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1192,6 +1214,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1242,6 +1265,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1293,6 +1317,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1343,6 +1368,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1396,6 +1422,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1448,6 +1475,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1500,6 +1528,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1552,6 +1581,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1605,6 +1635,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1658,6 +1689,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1709,6 +1741,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1759,6 +1792,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1810,6 +1844,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1860,6 +1895,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1913,6 +1949,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1965,6 +2002,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2018,6 +2056,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2071,6 +2110,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2122,6 +2162,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2172,6 +2213,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2223,6 +2265,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2273,6 +2316,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2326,6 +2370,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2379,6 +2424,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2432,6 +2478,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2483,6 +2530,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2533,6 +2581,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2584,6 +2633,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2634,6 +2684,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py index eaa42e47257..d853a15bfc3 100644 --- a/tests/components/sensibo/conftest.py +++ b/tests/components/sensibo/conftest.py @@ -2,16 +2,15 @@ from __future__ import annotations +from collections.abc import AsyncGenerator, Generator import json from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch -from pysensibo import SensiboClient -from pysensibo.model import SensiboData +from pysensibo import SensiboClient, SensiboData import pytest from homeassistant.components.sensibo.const import DOMAIN, PLATFORMS -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -21,6 +20,15 @@ from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.sensibo.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + @pytest.fixture(name="load_platforms") async def patch_platform_constant() -> list[Platform]: """Return list of platforms to load.""" @@ -30,63 +38,84 @@ async def patch_platform_constant() -> list[Platform]: @pytest.fixture async def load_int( hass: HomeAssistant, - get_data: SensiboData, + mock_client: SensiboClient, load_platforms: list[Platform], ) -> MockConfigEntry: - """Set up the Sensibo integration in Home Assistant.""" + """Set up the Sensibo integration.""" config_entry = MockConfigEntry( domain=DOMAIN, - source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="username", + unique_id="firstnamelastname", version=2, ) config_entry.add_to_hass(hass) - with ( - patch("homeassistant.components.sensibo.PLATFORMS", load_platforms), - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - ): + with patch("homeassistant.components.sensibo.PLATFORMS", load_platforms): await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done() return config_entry +@pytest.fixture(name="mock_client") +async def get_client( + hass: HomeAssistant, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], +) -> AsyncGenerator[MagicMock]: + """Mock SensiboClient.""" + + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient", + autospec=True, + ) as mock_client, + patch("homeassistant.components.sensibo.util.SensiboClient", new=mock_client), + ): + client = mock_client.return_value + client.async_get_devices_data.return_value = get_data[0] + client.async_get_me.return_value = get_data[1] + client.async_get_devices.return_value = get_data[2] + yield client + + @pytest.fixture(name="get_data") async def get_data_from_library( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, load_json: dict[str, Any] -) -> SensiboData: - """Retrieve data from upstream Sensibo library.""" - - client = SensiboClient("123467890", aioclient_mock.create_session(hass.loop)) - with patch("pysensibo.SensiboClient.async_get_devices", return_value=load_json): - output = await client.async_get_devices_data() + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + load_json: tuple[dict[str, Any], dict[str, Any]], +) -> AsyncGenerator[tuple[SensiboData, dict[str, Any], dict[str, Any]]]: + """Get data from api.""" + client = SensiboClient("1234567890", aioclient_mock.create_session(hass.loop)) + with patch.object( + client, + "async_get_me", + return_value=load_json[1], + ): + me = await client.async_get_me() + with patch.object( + client, + "async_get_devices", + return_value=load_json[0], + ): + data = await client.async_get_devices_data() + raw_data = await client.async_get_devices() + yield (data, me, raw_data) await client._session.close() - return output @pytest.fixture(name="load_json") -def load_json_from_fixture(load_data: str) -> SensiboData: +def load_json_from_fixture( + load_data: tuple[str, str], +) -> tuple[dict[str, Any], dict[str, Any]]: """Load fixture with json data and return.""" - json_data: dict[str, Any] = json.loads(load_data) - return json_data + json_data: dict[str, Any] = json.loads(load_data[0]) + json_me: dict[str, Any] = json.loads(load_data[1]) + return (json_data, json_me) @pytest.fixture(name="load_data", scope="package") -def load_data_from_fixture() -> str: +def load_data_from_fixture() -> tuple[str, str]: """Load fixture with fixture data and return.""" - return load_fixture("data.json", "sensibo") + return (load_fixture("data.json", "sensibo"), load_fixture("me.json", "sensibo")) diff --git a/tests/components/sensibo/fixtures/me.json b/tests/components/sensibo/fixtures/me.json new file mode 100644 index 00000000000..1b06a524966 --- /dev/null +++ b/tests/components/sensibo/fixtures/me.json @@ -0,0 +1,13 @@ +{ + "status": "success", + "result": { + "username": "firstnamelastname", + "email": "first.last@domain.com", + "firstName": "Firstname", + "lastName": "Lastname", + "temperatureUnit": "C", + "appRegistrationSource": "Sensibo", + "organization": null, + "availableOrganizations": [] + } +} diff --git a/tests/components/sensibo/snapshots/test_binary_sensor.ambr b/tests/components/sensibo/snapshots/test_binary_sensor.ambr index 110a6ae8174..2e62c73acb4 100644 --- a/tests/components/sensibo/snapshots/test_binary_sensor.ambr +++ b/tests/components/sensibo/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -381,6 +389,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -428,6 +437,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -475,6 +485,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -522,6 +533,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -569,6 +581,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -616,6 +629,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -663,6 +677,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/snapshots/test_button.ambr b/tests/components/sensibo/snapshots/test_button.ambr index 7ef6d56c714..6bfc4a5a44f 100644 --- a/tests/components/sensibo/snapshots/test_button.ambr +++ b/tests/components/sensibo/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/snapshots/test_climate.ambr b/tests/components/sensibo/snapshots/test_climate.ambr index e3b27332932..e3bd456ad23 100644 --- a/tests/components/sensibo/snapshots/test_climate.ambr +++ b/tests/components/sensibo/snapshots/test_climate.ambr @@ -13,6 +13,7 @@ 'target_temp_step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -81,6 +82,11 @@ ]), 'max_temp': 20, 'min_temp': 10, + 'swing_horizontal_modes': list([ + 'stopped', + 'fixedleft', + 'fixedcenterleft', + ]), 'swing_modes': list([ 'stopped', 'fixedtop', @@ -89,6 +95,7 @@ 'target_temp_step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -109,7 +116,7 @@ 'original_name': None, 'platform': 'sensibo', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'climate_device', 'unique_id': 'ABC999111', 'unit_of_measurement': None, @@ -137,7 +144,13 @@ ]), 'max_temp': 20, 'min_temp': 10, - 'supported_features': , + 'supported_features': , + 'swing_horizontal_mode': 'stopped', + 'swing_horizontal_modes': list([ + 'stopped', + 'fixedleft', + 'fixedcenterleft', + ]), 'swing_mode': 'stopped', 'swing_modes': list([ 'stopped', @@ -174,6 +187,7 @@ 'target_temp_step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,7 +208,7 @@ 'original_name': None, 'platform': 'sensibo', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'climate_device', 'unique_id': 'AAZZAAZZ', 'unit_of_measurement': None, @@ -216,9 +230,8 @@ ]), 'max_temp': 1, 'min_temp': 0, - 'supported_features': , + 'supported_features': , 'target_temp_step': 1, - 'temperature': None, }), 'context': , 'entity_id': 'climate.kitchen', @@ -228,3 +241,23 @@ 'state': 'off', }) # --- +# name: test_climate_get_device_capabilities + dict({ + 'climate.hallway': dict({ + 'horizontalswing': list([ + 'stopped', + 'fixedLeft', + 'fixedCenterLeft', + ]), + 'light': list([ + 'on', + 'off', + ]), + 'swing': list([ + 'stopped', + 'fixedTop', + 'fixedMiddleTop', + ]), + }), + }) +# --- diff --git a/tests/components/sensibo/snapshots/test_entity.ambr b/tests/components/sensibo/snapshots/test_entity.ambr new file mode 100644 index 00000000000..80ee847cb55 --- /dev/null +++ b/tests/components/sensibo/snapshots/test_entity.ambr @@ -0,0 +1,141 @@ +# serializer version: 1 +# name: test_device + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': 'hallway', + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://home.sensibo.com/', + 'connections': set({ + tuple( + 'mac', + '00:02:00:b6:00:00', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': 'esp8266ex', + 'id': , + 'identifiers': set({ + tuple( + 'sensibo', + 'ABC999111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Sensibo', + 'model': 'skyv2', + 'model_id': None, + 'name': 'Hallway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '1234567890', + 'suggested_area': 'Hallway', + 'sw_version': 'SKY30046', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': 'kitchen', + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://home.sensibo.com/', + 'connections': set({ + tuple( + 'mac', + '00:01:00:01:00:01', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': 'pure-esp32', + 'id': , + 'identifiers': set({ + tuple( + 'sensibo', + 'AAZZAAZZ', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Sensibo', + 'model': 'pure', + 'model_id': None, + 'name': 'Kitchen', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '0987654321', + 'suggested_area': 'Kitchen', + 'sw_version': 'PUR00111', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': 'bedroom', + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://home.sensibo.com/', + 'connections': set({ + tuple( + 'mac', + '00:03:00:03:00:03', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': 'pure-esp32', + 'id': , + 'identifiers': set({ + tuple( + 'sensibo', + 'BBZZBBZZ', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Sensibo', + 'model': 'pure', + 'model_id': None, + 'name': 'Bedroom', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '0987654329', + 'suggested_area': 'Bedroom', + 'sw_version': 'PUR00111', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://home.sensibo.com/', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': 'nrf52', + 'id': , + 'identifiers': set({ + tuple( + 'sensibo', + 'AABBCC', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Sensibo', + 'model': 'motion_sensor', + 'model_id': None, + 'name': 'Hallway Motion Sensor', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'V17', + 'via_device_id': , + }), + ]) +# --- diff --git a/tests/components/sensibo/snapshots/test_number.ambr b/tests/components/sensibo/snapshots/test_number.ambr index b632b95f1be..458c7ca7183 100644 --- a/tests/components/sensibo/snapshots/test_number.ambr +++ b/tests/components/sensibo/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -68,6 +69,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -125,6 +127,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -182,6 +185,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -239,6 +243,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -296,6 +301,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/snapshots/test_select.ambr b/tests/components/sensibo/snapshots/test_select.ambr index bdafc8654ff..05582a1ea16 100644 --- a/tests/components/sensibo/snapshots/test_select.ambr +++ b/tests/components/sensibo/snapshots/test_select.ambr @@ -1,61 +1,4 @@ # serializer version: 1 -# name: test_select[load_platforms0][select.hallway_horizontal_swing-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'stopped', - 'fixedleft', - 'fixedcenterleft', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': None, - 'entity_id': 'select.hallway_horizontal_swing', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Horizontal swing', - 'platform': 'sensibo', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'horizontalswing', - 'unique_id': 'ABC999111-horizontalSwing', - 'unit_of_measurement': None, - }) -# --- -# name: test_select[load_platforms0][select.hallway_horizontal_swing-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Hallway Horizontal swing', - 'options': list([ - 'stopped', - 'fixedleft', - 'fixedcenterleft', - ]), - }), - 'context': , - 'entity_id': 'select.hallway_horizontal_swing', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'stopped', - }) -# --- # name: test_select[load_platforms0][select.hallway_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -68,6 +11,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -124,6 +68,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/snapshots/test_sensor.ambr b/tests/components/sensibo/snapshots/test_sensor.ambr index 31e579d9929..bfd5f2d3e9a 100644 --- a/tests/components/sensibo/snapshots/test_sensor.ambr +++ b/tests/components/sensibo/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -111,6 +113,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -159,6 +162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -218,6 +222,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -275,6 +280,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -321,6 +327,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -370,6 +377,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -421,6 +429,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -472,6 +481,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -523,6 +533,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -574,6 +585,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -623,6 +635,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -672,6 +685,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -725,6 +739,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -777,6 +792,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/snapshots/test_switch.ambr b/tests/components/sensibo/snapshots/test_switch.ambr index 13cb73cef7a..e0ea140eb37 100644 --- a/tests/components/sensibo/snapshots/test_switch.ambr +++ b/tests/components/sensibo/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -101,6 +103,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/snapshots/test_update.ambr b/tests/components/sensibo/snapshots/test_update.ambr index 3eb69c9a812..c113d5615b1 100644 --- a/tests/components/sensibo/snapshots/test_update.ambr +++ b/tests/components/sensibo/snapshots/test_update.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -124,6 +126,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index dbc3e87a236..771b4a0b6b5 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -3,17 +3,16 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock -from pysensibo.model import SensiboData +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, snapshot_platform @@ -26,33 +25,27 @@ from tests.common import async_fire_time_changed, snapshot_platform async def test_binary_sensor( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo binary sensor.""" await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) - monkeypatch.setattr( - get_data.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False - ) - monkeypatch.setattr( - get_data.parsed["ABC999111"].motion_sensors["AABBCC"], "motion", False - ) + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].motion_sensors[ + "AABBCC" + ].motion = False - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state1 = hass.states.get("binary_sensor.hallway_motion_sensor_connectivity") - state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") - assert state1.state == "off" - assert state3.state == "off" + assert ( + hass.states.get("binary_sensor.hallway_motion_sensor_connectivity").state + == STATE_ON + ) + assert ( + hass.states.get("binary_sensor.hallway_motion_sensor_motion").state == STATE_OFF + ) diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py index 5c36fe9e94d..2d93fd81ed7 100644 --- a/tests/components/sensibo/test_button.py +++ b/tests/components/sensibo/test_button.py @@ -2,12 +2,11 @@ from __future__ import annotations -from datetime import datetime, timedelta -from unittest.mock import patch +from datetime import timedelta +from unittest.mock import MagicMock from freezegun import freeze_time from freezegun.api import FrozenDateTimeFactory -from pysensibo.model import SensiboData import pytest from syrupy.assertion import SnapshotAssertion @@ -45,14 +44,18 @@ async def test_button( await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + "load_platforms", + [[Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR]], +) async def test_button_update( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, freezer: FrozenDateTimeFactory, ) -> None: - """Test the Sensibo button.""" + """Test the Sensibo button press.""" state_button = hass.states.get("button.hallway_reset_filter") state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") @@ -62,48 +65,32 @@ async def test_button_update( assert state_filter_clean.state is STATE_ON assert state_filter_last_reset.state == "2022-03-12T15:24:26+00:00" - today = datetime(datetime.now().year + 1, 6, 19, 20, 0, 0).replace( - tzinfo=dt_util.UTC - ) - today_str = today.isoformat() + today = dt_util.utcnow() + timedelta(minutes=10) + today = today.replace(microsecond=0) + today_str = today.isoformat(timespec="seconds") freezer.move_to(today) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", - return_value={"status": "success"}, - ), - ): - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - { - ATTR_ENTITY_ID: state_button.entity_id, - }, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_reset_filter.return_value = {"status": "success"} - monkeypatch.setattr(get_data.parsed["ABC999111"], "filter_clean", False) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "filter_last_reset", - today, + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: state_button.entity_id, + }, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].filter_clean = False + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].filter_last_reset = today + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() state_button = hass.states.get("button.hallway_reset_filter") state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") @@ -116,31 +103,22 @@ async def test_button_update( async def test_button_failure( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, ) -> None: - """Test the Sensibo button fails.""" + """Test the Sensibo button failure.""" - state_button = hass.states.get("button.hallway_reset_filter") + state = hass.states.get("button.hallway_reset_filter") - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", - return_value={"status": "failure"}, - ), - pytest.raises( - HomeAssistantError, - ), + mock_client.async_reset_filter.return_value = {"status": "failure"} + + with pytest.raises( + HomeAssistantError, ): await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, { - ATTR_ENTITY_ID: state_button.entity_id, + ATTR_ENTITY_ID: state.entity_id, }, blocking=True, ) diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 7916727e57a..7e848f3870c 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -1,11 +1,11 @@ -"""The test for the sensibo binary sensor platform.""" +"""The test for the sensibo climate platform.""" from __future__ import annotations from datetime import datetime, timedelta -from unittest.mock import patch +from unittest.mock import MagicMock -from pysensibo.model import SensiboData +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from voluptuous import MultipleInvalid @@ -13,14 +13,17 @@ from voluptuous import MultipleInvalid from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, + ATTR_SWING_HORIZONTAL_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_HORIZONTAL_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + HVACMode, ) from homeassistant.components.sensibo.climate import ( ATTR_AC_INTEGRATION, @@ -42,6 +45,7 @@ from homeassistant.components.sensibo.climate import ( SERVICE_ENABLE_PURE_BOOST, SERVICE_ENABLE_TIMER, SERVICE_FULL_STATE, + SERVICE_GET_DEVICE_CAPABILITIES, _find_valid_target_temp, ) from homeassistant.components.sensibo.const import DOMAIN @@ -53,6 +57,8 @@ from homeassistant.const import ( ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, Platform, ) @@ -86,7 +92,6 @@ async def test_climate_find_valid_targets() -> None: async def test_climate( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - get_data: SensiboData, load_int: ConfigEntry, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, @@ -95,45 +100,39 @@ async def test_climate( await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) - found_log = False logs = caplog.get_records("setup") - for log in logs: - if ( - log.message - == "Device Bedroom not correctly registered with remote on Sensibo cloud." - ): - found_log = True - break - assert found_log + assert "Device Bedroom not correctly registered with remote on Sensibo cloud." in [ + log.message for log in logs + ] async def test_climate_fan( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate fan service.""" - state1 = hass.states.get("climate.hallway") - assert state1.attributes["fan_mode"] == "high" + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "high" + + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].fan_modes = [ + "quiet", + "low", + "medium", + "not_in_ha", + ] + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].fan_modes_translated = { + "low": "low", + "medium": "medium", + "quiet": "quiet", + "not_in_ha": "not_in_ha", + } - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "fan_modes", - ["quiet", "low", "medium", "not_in_ha"], - ) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "fan_modes_translated", - { - "low": "low", - "medium": "medium", - "quiet": "quiet", - "not_in_ha": "not_in_ha", - }, - ) with pytest.raises( HomeAssistantError, match="Climate fan mode not_in_ha is not supported by the integration", @@ -141,107 +140,81 @@ async def test_climate_fan( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "not_in_ha"}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "not_in_ha"}, blocking=True, ) - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "low"}, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } - state2 = hass.states.get("climate.hallway") - assert state2.attributes["fan_mode"] == "low" - - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "swing", - "targetTemperature", - "horizontalSwing", - "light", - ], + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), - pytest.raises(HomeAssistantError), - ): + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "low" + + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].active_features = [ + "timestamp", + "on", + "mode", + "swing", + "targetTemperature", + "horizontalSwing", + "light", + ] + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match="service_not_supported"): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "low"}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, blocking=True, ) - await hass.async_block_till_done() - state3 = hass.states.get("climate.hallway") - assert state3.attributes["fan_mode"] == "low" + state = hass.states.get("climate.hallway") + assert "fan_mode" not in state.attributes async def test_climate_swing( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate swing service.""" - state1 = hass.states.get("climate.hallway") - assert state1.attributes["swing_mode"] == "stopped" + state = hass.states.get("climate.hallway") + assert state.attributes["swing_mode"] == "stopped" - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "swing_modes", - ["stopped", "fixedtop", "fixedmiddletop", "not_in_ha"], - ) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "swing_modes_translated", - { - "fixedmiddletop": "fixedMiddleTop", - "fixedtop": "fixedTop", - "stopped": "stopped", - "not_in_ha": "not_in_ha", - }, - ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].swing_modes = [ + "stopped", + "fixedtop", + "fixedmiddletop", + "not_in_ha", + ] + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].swing_modes_translated = { + "fixedmiddletop": "fixedMiddleTop", + "fixedtop": "fixedTop", + "stopped": "stopped", + "not_in_ha": "not_in_ha", + } + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() with pytest.raises( HomeAssistantError, @@ -250,593 +223,506 @@ async def test_climate_swing( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "not_in_ha"}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_SWING_MODE: "not_in_ha"}, blocking=True, ) - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedtop"}, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } - state2 = hass.states.get("climate.hallway") - assert state2.attributes["swing_mode"] == "fixedtop" - - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "targetTemperature", - "horizontalSwing", - "light", - ], + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_SWING_MODE: "fixedtop"}, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), - pytest.raises(HomeAssistantError), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedtop"}, - blocking=True, - ) + state = hass.states.get("climate.hallway") + assert state.attributes["swing_mode"] == "fixedtop" + + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].active_features = [ + "timestamp", + "on", + "mode", + "targetTemperature", + "horizontalSwing", + "light", + ] + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - state3 = hass.states.get("climate.hallway") - assert state3.attributes["swing_mode"] == "fixedtop" + with pytest.raises(HomeAssistantError, match="service_not_supported"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_SWING_MODE: "fixedtop"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert "swing_mode" not in state.attributes + + +async def test_climate_horizontal_swing( + hass: HomeAssistant, + load_int: ConfigEntry, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Sensibo climate horizontal swing service.""" + + state = hass.states.get("climate.hallway") + assert state.attributes["swing_horizontal_mode"] == "stopped" + + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].horizontal_swing_modes = [ + "stopped", + "fixedleft", + "fixedcenter", + "fixedright", + "not_in_ha", + ] + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].swing_modes_translated = { + "stopped": "stopped", + "fixedleft": "fixedLeft", + "fixedcenter": "fixedCenter", + "fixedright": "fixedRight", + "not_in_ha": "not_in_ha", + } + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + with pytest.raises( + HomeAssistantError, + match="Climate horizontal swing mode not_in_ha is not supported by the integration", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_HORIZONTAL_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_SWING_HORIZONTAL_MODE: "not_in_ha"}, + blocking=True, + ) + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_HORIZONTAL_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_SWING_HORIZONTAL_MODE: "fixedleft"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.attributes["swing_horizontal_mode"] == "fixedleft" + + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].active_features = [ + "timestamp", + "on", + "mode", + "targetTemperature", + "swing", + "light", + ] + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match="service_not_supported"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_HORIZONTAL_MODE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_SWING_HORIZONTAL_MODE: "fixedcenter", + }, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert "swing_horizontal_mode" not in state.attributes async def test_climate_temperatures( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate temperature service.""" - state1 = hass.states.get("climate.hallway") - assert state1.attributes["temperature"] == 25 + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 25 - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 20 - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_set_ac_state_property", - ) as mock_call: - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, - blocking=True, - ) - assert not mock_call.called - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 15}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 16 - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 18.5}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 19 - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - pytest.raises( - ServiceValidationError, - match="Provided temperature 24.0 is not valid. Accepted range is 10 to 20", - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 24}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 19 - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 20 - - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - pytest.raises(MultipleInvalid), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 20 - - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "swing", - "horizontalSwing", - "light", - ], + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - pytest.raises(HomeAssistantError), + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 20 + + mock_client.async_set_ac_state_property.reset_mock() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + assert not mock_client.async_set_ac_state_property.called + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 16 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 18.5}, + blocking=True, + ) + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 19 + + with pytest.raises( + ServiceValidationError, + match="Provided temperature 24.0 is not valid. Accepted range is 10 to 20", ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 24}, blocking=True, ) + + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 19 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 20 + + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 20 + + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].active_features = [ + "timestamp", + "on", + "mode", + "swing", + "horizontalSwing", + "light", + ] + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 20 + with pytest.raises(HomeAssistantError, match="service_not_supported"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert "temperature" not in state.attributes async def test_climate_temperature_is_none( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate temperature service no temperature provided.""" - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "fanLevel", - "targetTemperature", - "swing", - "horizontalSwing", - "light", - ], - ) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "target_temp", - 25, - ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].active_features = [ + "timestamp", + "on", + "mode", + "fanLevel", + "targetTemperature", + "swing", + "horizontalSwing", + "light", + ] + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].target_temp = 25 - state1 = hass.states.get("climate.hallway") - assert state1.attributes["temperature"] == 25 + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), - pytest.raises(ServiceValidationError), - ): + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 25 + + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: state1.entity_id, + ATTR_ENTITY_ID: state.entity_id, ATTR_TARGET_TEMP_HIGH: 30, ATTR_TARGET_TEMP_LOW: 20, }, blocking=True, ) - await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 25 + state = hass.states.get("climate.hallway") + assert state.attributes["temperature"] == 25 async def test_climate_hvac_mode( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate hvac mode service.""" - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "fanLevel", - "targetTemperature", - "swing", - "horizontalSwing", - "light", - ], - ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].active_features = [ + "timestamp", + "on", + "mode", + "fanLevel", + "targetTemperature", + "swing", + "horizontalSwing", + "light", + ] - state1 = hass.states.get("climate.hallway") - assert state1.state == "heat" - - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "off"}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.state == "off" - - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", False) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "heat"}, - blocking=True, - ) + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.state == "heat" + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_HVAC_MODE: "off"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.OFF + + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].device_on = False + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_HVAC_MODE: "heat"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT async def test_climate_on_off( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate on/off service.""" - monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].hvac_mode = "heat" + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].device_on = True - state1 = hass.states.get("climate.hallway") - assert state1.state == "heat" - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: state1.entity_id}, - blocking=True, - ) - await hass.async_block_till_done() - - state2 = hass.states.get("climate.hallway") - assert state2.state == "off" - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: state1.entity_id}, - blocking=True, - ) + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.state == "heat" + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: state.entity_id}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.OFF + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: state.entity_id}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT async def test_climate_service_failed( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate service failed.""" - monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].hvac_mode = "heat" + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].device_on = True - state1 = hass.states.get("climate.hallway") - assert state1.state == "heat" + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={ - "result": {"status": "Error", "failureReason": "Did not work"} - }, - ), - pytest.raises(HomeAssistantError), - ): + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Error", "failureReason": "Did not work"} + } + + with pytest.raises(HomeAssistantError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: state1.entity_id}, + {ATTR_ENTITY_ID: state.entity_id}, blocking=True, ) - await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.state == "heat" + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT async def test_climate_assumed_state( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate assumed state service.""" - monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].hvac_mode = "heat" + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].device_on = True - state1 = hass.states.get("climate.hallway") - assert state1.state == "heat" - - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - DOMAIN, - SERVICE_ASSUME_STATE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_STATE: "off"}, - blocking=True, - ) + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.state == "off" + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + DOMAIN, + SERVICE_ASSUME_STATE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_STATE: "off"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.OFF async def test_climate_no_fan_no_swing( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the Sensibo climate fan service.""" + """Test the Sensibo climate fan.""" state = hass.states.get("climate.hallway") assert state.attributes["fan_mode"] == "high" assert state.attributes["swing_mode"] == "stopped" - monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_mode", None) - monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_mode", None) - monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_modes", None) - monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_modes", None) + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].fan_mode = None + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].swing_mode = None + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].fan_modes = None + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].swing_modes = None - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() state = hass.states.get("climate.hallway") assert state.attributes["fan_mode"] is None @@ -849,110 +735,70 @@ async def test_climate_no_fan_no_swing( async def test_climate_set_timer( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate Set Timer service.""" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state_climate = hass.states.get("climate.hallway") + state = hass.states.get("climate.hallway") assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ), - pytest.raises( - MultipleInvalid, - ), - ): + mock_client.async_set_timer.return_value = {"status": "failure"} + + with pytest.raises(MultipleInvalid): await hass.services.async_call( DOMAIN, SERVICE_ENABLE_TIMER, { - ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_ENTITY_ID: state.entity_id, }, blocking=True, ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ), - pytest.raises( - HomeAssistantError, - ), - ): + with pytest.raises(HomeAssistantError): await hass.services.async_call( DOMAIN, SERVICE_ENABLE_TIMER, { - ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_ENTITY_ID: state.entity_id, ATTR_MINUTES: 30, }, blocking=True, ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, - ), - ): - await hass.services.async_call( - DOMAIN, - SERVICE_ENABLE_TIMER, - { - ATTR_ENTITY_ID: state_climate.entity_id, - ATTR_MINUTES: 30, - }, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_timer.return_value = { + "status": "success", + "result": {"id": "SzTGE4oZ4D"}, + } - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", True) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", "SzTGE4oZ4D") - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", False) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "timer_time", - datetime(2022, 6, 6, 12, 00, 00, tzinfo=dt_util.UTC), + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_TIMER, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_MINUTES: 30, + }, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].timer_on = True + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].timer_id = "SzTGE4oZ4D" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].timer_state_on = False + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].timer_time = datetime(2022, 6, 6, 12, 00, 00, tzinfo=dt_util.UTC) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() assert ( hass.states.get("sensor.hallway_timer_end_time").state @@ -964,144 +810,110 @@ async def test_climate_set_timer( async def test_climate_pure_boost( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the Sensibo climate assumed state service.""" + """Test the Sensibo climate pure boost service.""" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state_climate = hass.states.get("climate.kitchen") + state = hass.states.get("climate.kitchen") state2 = hass.states.get("switch.kitchen_pure_boost") - assert state2.state == "off" + assert state2.state == STATE_OFF - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - ), - pytest.raises( - MultipleInvalid, - ), + with pytest.raises( + MultipleInvalid, ): await hass.services.async_call( DOMAIN, SERVICE_ENABLE_PURE_BOOST, { - ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_ENTITY_ID: state.entity_id, ATTR_INDOOR_INTEGRATION: True, ATTR_OUTDOOR_INTEGRATION: True, - ATTR_SENSITIVITY: "Sensitive", + ATTR_SENSITIVITY: "sensitive", }, blocking=True, ) + + mock_client.async_set_pureboost.return_value = { + "status": "success", + "result": { + "enabled": True, + "sensitivity": "S", + "measurements_integration": True, + "ac_integration": False, + "geo_integration": False, + "prime_integration": True, + }, + } + + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_PURE_BOOST, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_AC_INTEGRATION: False, + ATTR_GEO_INTEGRATION: False, + ATTR_INDOOR_INTEGRATION: True, + ATTR_OUTDOOR_INTEGRATION: True, + ATTR_SENSITIVITY: "sensitive", + }, + blocking=True, + ) + + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_boost_enabled = True + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_sensitivity = "s" + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_measure_integration = True + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_prime_integration = True + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - return_value={ - "status": "success", - "result": { - "enabled": True, - "sensitivity": "S", - "measurements_integration": True, - "ac_integration": False, - "geo_integration": False, - "prime_integration": True, - }, - }, - ), - ): - await hass.services.async_call( - DOMAIN, - SERVICE_ENABLE_PURE_BOOST, - { - ATTR_ENTITY_ID: state_climate.entity_id, - ATTR_AC_INTEGRATION: False, - ATTR_GEO_INTEGRATION: False, - ATTR_INDOOR_INTEGRATION: True, - ATTR_OUTDOOR_INTEGRATION: True, - ATTR_SENSITIVITY: "Sensitive", - }, - blocking=True, - ) - await hass.async_block_till_done() - - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True) - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_sensitivity", "s") - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", True) - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_prime_integration", True) - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state1 = hass.states.get("switch.kitchen_pure_boost") - state2 = hass.states.get( - "binary_sensor.kitchen_pure_boost_linked_with_indoor_air_quality" + assert hass.states.get("switch.kitchen_pure_boost").state == STATE_ON + assert ( + hass.states.get( + "binary_sensor.kitchen_pure_boost_linked_with_indoor_air_quality" + ).state + == STATE_ON ) - state3 = hass.states.get( - "binary_sensor.kitchen_pure_boost_linked_with_outdoor_air_quality" + assert ( + hass.states.get( + "binary_sensor.kitchen_pure_boost_linked_with_outdoor_air_quality" + ).state + == STATE_ON ) - state4 = hass.states.get("sensor.kitchen_pure_sensitivity") - assert state1.state == "on" - assert state2.state == "on" - assert state3.state == "on" - assert state4.state == "s" + assert hass.states.get("sensor.kitchen_pure_sensitivity").state == "s" @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_climate_react( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate react custom service.""" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() state_climate = hass.states.get("climate.hallway") - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", - ), - pytest.raises( - MultipleInvalid, - ), + with pytest.raises( + MultipleInvalid, ): await hass.services.async_call( DOMAIN, @@ -1114,423 +926,386 @@ async def test_climate_climate_react( }, blocking=True, ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", - return_value={ - "status": "success", - "result": { - "enabled": True, - "deviceUid": "ABC999111", - "highTemperatureState": { - "on": True, - "targetTemperature": 15, - "temperatureUnit": "C", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "highTemperatureThreshold": 30.5, - "lowTemperatureState": { - "on": True, - "targetTemperature": 25, - "temperatureUnit": "C", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "lowTemperatureThreshold": 5.5, - "type": "temperature", - }, + mock_client.async_set_climate_react.return_value = { + "status": "success", + "result": { + "enabled": True, + "deviceUid": "ABC999111", + "highTemperatureState": { + "on": True, + "targetTemperature": 15, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", }, - ), - ): - await hass.services.async_call( - DOMAIN, - SERVICE_ENABLE_CLIMATE_REACT, - { - ATTR_ENTITY_ID: state_climate.entity_id, - ATTR_LOW_TEMPERATURE_THRESHOLD: 5.5, - ATTR_HIGH_TEMPERATURE_THRESHOLD: 30.5, - ATTR_LOW_TEMPERATURE_STATE: { - "on": True, - "targetTemperature": 25, - "temperatureUnit": "C", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - ATTR_HIGH_TEMPERATURE_STATE: { - "on": True, - "targetTemperature": 15, - "temperatureUnit": "C", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - ATTR_SMART_TYPE: "temperature", + "highTemperatureThreshold": 30.5, + "lowTemperatureState": { + "on": True, + "targetTemperature": 25, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", }, - blocking=True, - ) - await hass.async_block_till_done() + "lowTemperatureThreshold": 5.5, + "type": "feelsLike", + }, + } - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True) - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", "temperature") - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_low_temp_threshold", 5.5) - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_high_temp_threshold", 30.5) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "smart_low_state", + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_CLIMATE_REACT, { - "on": True, - "targetTemperature": 25, - "temperatureUnit": "C", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - ) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "smart_high_state", - { - "on": True, - "targetTemperature": 15, - "temperatureUnit": "C", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", + ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_LOW_TEMPERATURE_THRESHOLD: 5.5, + ATTR_HIGH_TEMPERATURE_THRESHOLD: 30.5, + ATTR_LOW_TEMPERATURE_STATE: { + "on": True, + "targetTemperature": 25, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + ATTR_HIGH_TEMPERATURE_STATE: { + "on": True, + "targetTemperature": 15, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + ATTR_SMART_TYPE: "feelslike", }, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].smart_on = True + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_type = "feelsLike" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_low_temp_threshold = 5.5 + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_high_temp_threshold = 30.5 + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_low_state = { + "on": True, + "targetTemperature": 25, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + } + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_high_state = { + "on": True, + "targetTemperature": 15, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + } - state1 = hass.states.get("switch.hallway_climate_react") - state2 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold") - state3 = hass.states.get("sensor.hallway_climate_react_high_temperature_threshold") - state4 = hass.states.get("sensor.hallway_climate_react_type") - assert state1.state == "on" - assert state2.state == "5.5" - assert state3.state == "30.5" - assert state4.state == "temperature" + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("switch.hallway_climate_react").state == STATE_ON + assert ( + hass.states.get("sensor.hallway_climate_react_low_temperature_threshold").state + == "5.5" + ) + assert ( + hass.states.get("sensor.hallway_climate_react_high_temperature_threshold").state + == "30.5" + ) + assert hass.states.get("sensor.hallway_climate_react_type").state == "feelslike" @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_climate_react_fahrenheit( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate react custom service with fahrenheit.""" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state_climate = hass.states.get("climate.hallway") - - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", - return_value={ - "status": "success", - "result": { - "enabled": True, - "deviceUid": "ABC999111", - "highTemperatureState": { - "on": True, - "targetTemperature": 65, - "temperatureUnit": "F", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "highTemperatureThreshold": 77, - "lowTemperatureState": { - "on": True, - "targetTemperature": 85, - "temperatureUnit": "F", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "lowTemperatureThreshold": 32, - "type": "temperature", - }, - }, - ), - ): - await hass.services.async_call( - DOMAIN, - SERVICE_ENABLE_CLIMATE_REACT, - { - ATTR_ENTITY_ID: state_climate.entity_id, - ATTR_LOW_TEMPERATURE_THRESHOLD: 32.0, - ATTR_HIGH_TEMPERATURE_THRESHOLD: 77.0, - ATTR_LOW_TEMPERATURE_STATE: { - "on": True, - "targetTemperature": 85, - "temperatureUnit": "F", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - ATTR_HIGH_TEMPERATURE_STATE: { - "on": True, - "targetTemperature": 65, - "temperatureUnit": "F", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - ATTR_SMART_TYPE: "temperature", - }, - blocking=True, - ) + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True) - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", "temperature") - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_low_temp_threshold", 0) - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_high_temp_threshold", 25) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "smart_low_state", - { - "on": True, - "targetTemperature": 85, - "temperatureUnit": "F", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", + state = hass.states.get("climate.hallway") + + mock_client.async_set_climate_react.return_value = { + "status": "success", + "result": { + "enabled": True, + "deviceUid": "ABC999111", + "highTemperatureState": { + "on": True, + "targetTemperature": 65, + "temperatureUnit": "F", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "highTemperatureThreshold": 77, + "lowTemperatureState": { + "on": True, + "targetTemperature": 85, + "temperatureUnit": "F", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "lowTemperatureThreshold": 32, + "type": "temperature", }, - ) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "smart_high_state", + } + + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_CLIMATE_REACT, { - "on": True, - "targetTemperature": 65, - "temperatureUnit": "F", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", + ATTR_ENTITY_ID: state.entity_id, + ATTR_LOW_TEMPERATURE_THRESHOLD: 32.0, + ATTR_HIGH_TEMPERATURE_THRESHOLD: 77.0, + ATTR_LOW_TEMPERATURE_STATE: { + "on": True, + "targetTemperature": 85, + "temperatureUnit": "F", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + ATTR_HIGH_TEMPERATURE_STATE: { + "on": True, + "targetTemperature": 65, + "temperatureUnit": "F", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + ATTR_SMART_TYPE: "temperature", }, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].smart_on = True + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_type = "temperature" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_low_temp_threshold = 0 + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_high_temp_threshold = 25 + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_low_state = { + "on": True, + "targetTemperature": 85, + "temperatureUnit": "F", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + } + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_high_state = { + "on": True, + "targetTemperature": 65, + "temperatureUnit": "F", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + } - state1 = hass.states.get("switch.hallway_climate_react") - state2 = hass.states.get("sensor.hallway_climate_react_low_temperature_threshold") - state3 = hass.states.get("sensor.hallway_climate_react_high_temperature_threshold") - state4 = hass.states.get("sensor.hallway_climate_react_type") - assert state1.state == "on" - assert state2.state == "0" - assert state3.state == "25" - assert state4.state == "temperature" + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("switch.hallway_climate_react").state == STATE_ON + assert ( + hass.states.get("sensor.hallway_climate_react_low_temperature_threshold").state + == "0" + ) + assert ( + hass.states.get("sensor.hallway_climate_react_high_temperature_threshold").state + == "25" + ) + assert hass.states.get("sensor.hallway_climate_react_type").state == "temperature" @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_climate_full_ac_state( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate Full AC state service.""" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state_climate = hass.states.get("climate.hallway") - assert state_climate.state == "heat" + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", - ), - pytest.raises( - MultipleInvalid, - ), + with pytest.raises( + MultipleInvalid, ): await hass.services.async_call( DOMAIN, SERVICE_FULL_STATE, { - ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_ENTITY_ID: state.entity_id, ATTR_TARGET_TEMPERATURE: 22, }, blocking=True, ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", - return_value={"result": {"status": "Success"}}, - ), - ): - await hass.services.async_call( - DOMAIN, - SERVICE_FULL_STATE, - { - ATTR_ENTITY_ID: state_climate.entity_id, - ATTR_MODE: "cool", - ATTR_TARGET_TEMPERATURE: 22, - ATTR_FAN_MODE: "high", - ATTR_SWING_MODE: "stopped", - ATTR_HORIZONTAL_SWING_MODE: "stopped", - ATTR_LIGHT: "on", - }, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_ac_states.return_value = {"result": {"status": "Success"}} - monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "cool") - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) - monkeypatch.setattr(get_data.parsed["ABC999111"], "target_temp", 22) - monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_mode", "high") - monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_mode", "stopped") - monkeypatch.setattr( - get_data.parsed["ABC999111"], "horizontal_swing_mode", "stopped" + await hass.services.async_call( + DOMAIN, + SERVICE_FULL_STATE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_MODE: "cool", + ATTR_TARGET_TEMPERATURE: 22, + ATTR_FAN_MODE: "high", + ATTR_SWING_MODE: "stopped", + ATTR_HORIZONTAL_SWING_MODE: "stopped", + ATTR_LIGHT: "on", + }, + blocking=True, ) - monkeypatch.setattr(get_data.parsed["ABC999111"], "light_mode", "on") - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].hvac_mode = "cool" + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].device_on = True + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].target_temp = 22 + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].fan_mode = "high" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].swing_mode = "stopped" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].horizontal_swing_mode = "stopped" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].light_mode = "on" + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() state = hass.states.get("climate.hallway") - assert state.state == "cool" + assert state.state == HVACMode.COOL assert state.attributes["temperature"] == 22 async def test_climate_fan_mode_and_swing_mode_not_supported( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo climate fan_mode and swing_mode not supported is raising error.""" - state1 = hass.states.get("climate.hallway") - assert state1.attributes["fan_mode"] == "high" - assert state1.attributes["swing_mode"] == "stopped" + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "high" + assert state.attributes["swing_mode"] == "stopped" - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), - pytest.raises(ServiceValidationError), - ): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "faulty_swing_mode"}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_SWING_MODE: "faulty_swing_mode"}, blocking=True, ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), - pytest.raises(ServiceValidationError), - ): + with pytest.raises(ServiceValidationError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "faulty_fan_mode"}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "faulty_fan_mode"}, blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state2 = hass.states.get("climate.hallway") - assert state2.attributes["fan_mode"] == "high" - assert state2.attributes["swing_mode"] == "stopped" + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "high" + assert state.attributes["swing_mode"] == "stopped" + + +async def test_climate_get_device_capabilities( + hass: HomeAssistant, + load_int: ConfigEntry, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test the Sensibo climate Get device capabilitites service.""" + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_DEVICE_CAPABILITIES, + {ATTR_ENTITY_ID: "climate.hallway", ATTR_HVAC_MODE: "heat"}, + blocking=True, + return_response=True, + ) + assert response == snapshot + + with pytest.raises( + ServiceValidationError, match="The entity does not support the chosen mode" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_DEVICE_CAPABILITIES, + {ATTR_ENTITY_ID: "climate.hallway", ATTR_HVAC_MODE: "heat_cool"}, + blocking=True, + return_response=True, + ) diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index d6edb1c7ae0..179c57167c8 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -3,24 +3,26 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock -import aiohttp -from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo import AuthenticationError, SensiboData, SensiboError import pytest from homeassistant import config_entries +from homeassistant.components.sensibo.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -DOMAIN = "sensibo" +pytestmark = pytest.mark.usefixtures("mock_setup_entry") -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" +async def test_basic_setup( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_client: MagicMock +) -> None: + """Test we get and complete the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -29,32 +31,19 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_API_KEY: "1234567890", - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + }, + ) - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["version"] == 2 - assert result2["data"] == { - "api_key": "1234567890", + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["version"] == 2 + assert result["title"] == "firstnamelastname" + assert result["result"].unique_id == "firstnamelastname" + assert result["data"] == { + CONF_API_KEY: "1234567890", } assert len(mock_setup_entry.mock_calls) == 1 @@ -63,14 +52,12 @@ async def test_form(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("error_message", "p_error"), [ - (aiohttp.ClientConnectionError, "cannot_connect"), - (TimeoutError, "cannot_connect"), (AuthenticationError, "invalid_auth"), (SensiboError, "cannot_connect"), ], ) async def test_flow_fails( - hass: HomeAssistant, error_message: Exception, p_error: str + hass: HomeAssistant, mock_client: MagicMock, error_message: Exception, p_error: str ) -> None: """Test config flow errors.""" @@ -81,48 +68,38 @@ async def test_flow_fails( assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - side_effect=error_message, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "1234567890", - }, - ) + mock_client.async_get_devices.side_effect = error_message - assert result2["errors"] == {"base": p_error} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ), - ): - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "1234567891", - }, - ) + assert result["errors"] == {"base": p_error} - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "Sensibo" - assert result3["data"] == { - "api_key": "1234567891", + mock_client.async_get_devices.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "firstnamelastname" + assert result["data"] == { + CONF_API_KEY: "1234567890", } -async def test_flow_get_no_devices(hass: HomeAssistant) -> None: +async def test_flow_get_no_devices( + hass: HomeAssistant, + mock_client: MagicMock, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], +) -> None: """Test config flow get no devices from api.""" result = await hass.config_entries.flow.async_init( @@ -132,27 +109,38 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": []}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {}}, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "1234567890", - }, - ) + mock_client.async_get_devices.return_value = {"result": []} - assert result2["errors"] == {"base": "no_devices"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result["errors"] == {"base": "no_devices"} + + mock_client.async_get_devices.return_value = get_data[2] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "firstnamelastname" + assert result["data"] == { + CONF_API_KEY: "1234567890", + } -async def test_flow_get_no_username(hass: HomeAssistant) -> None: +async def test_flow_get_no_username( + hass: HomeAssistant, + mock_client: MagicMock, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], +) -> None: """Test config flow get no username from api.""" result = await hass.config_entries.flow.async_init( @@ -162,33 +150,40 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {}}, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "1234567890", - }, - ) + mock_client.async_get_me.return_value = {"result": {}} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) assert result2["errors"] == {"base": "no_username"} + mock_client.async_get_me.return_value = get_data[1] -async def test_reauth_flow(hass: HomeAssistant) -> None: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "firstnamelastname" + assert result["data"] == { + CONF_API_KEY: "1234567890", + } + + +async def test_reauth_flow(hass: HomeAssistant, mock_client: MagicMock) -> None: """Test a reauthentication flow.""" entry = MockConfigEntry( version=2, domain=DOMAIN, - unique_id="username", - data={"api_key": "1234567890"}, + unique_id="firstnamelastname", + data={CONF_API_KEY: "1234567890"}, ) entry.add_to_hass(hass) @@ -197,94 +192,58 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ) as mock_sensibo, - patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "1234567891"}, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert entry.data == {"api_key": "1234567891"} - - assert len(mock_sensibo.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == {CONF_API_KEY: "1234567890"} @pytest.mark.parametrize( ("sideeffect", "p_error"), [ - (aiohttp.ClientConnectionError, "cannot_connect"), - (TimeoutError, "cannot_connect"), (AuthenticationError, "invalid_auth"), (SensiboError, "cannot_connect"), ], ) async def test_reauth_flow_error( - hass: HomeAssistant, sideeffect: Exception, p_error: str + hass: HomeAssistant, sideeffect: Exception, p_error: str, mock_client: MagicMock ) -> None: """Test a reauthentication flow with error.""" entry = MockConfigEntry( version=2, domain=DOMAIN, - unique_id="username", - data={"api_key": "1234567890"}, + unique_id="firstnamelastname", + data={CONF_API_KEY: "1234567890"}, ) entry.add_to_hass(hass) result = await entry.start_reauth_flow(hass) - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - side_effect=sideeffect, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "1234567890"}, - ) - await hass.async_block_till_done() + mock_client.async_get_devices.side_effect = sideeffect - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": p_error} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "1234567891"}, - ) - await hass.async_block_till_done() + assert result["step_id"] == "reauth_confirm" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": p_error} - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert entry.data == {"api_key": "1234567891"} + mock_client.async_get_devices.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == {CONF_API_KEY: "1234567890"} @pytest.mark.parametrize( @@ -297,12 +256,12 @@ async def test_reauth_flow_error( ), ( {"result": []}, - {"result": {"username": "username"}}, + {"result": {"username": "firstnamelastname"}}, "no_devices", ), ( {"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - {"result": {"username": "username2"}}, + {"result": {"username": "firstnamelastname2"}}, "incorrect_api_key", ), ], @@ -312,13 +271,15 @@ async def test_flow_reauth_no_username_or_device( get_devices: dict[str, Any], get_me: dict[str, Any], p_error: str, + mock_client: MagicMock, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], ) -> None: - """Test config flow get no username from api.""" + """Test reauth flow with errors from api.""" entry = MockConfigEntry( version=2, domain=DOMAIN, - unique_id="username", - data={"api_key": "1234567890"}, + unique_id="firstnamelastname", + data={CONF_API_KEY: "1234567890"}, ) entry.add_to_hass(hass) @@ -327,36 +288,40 @@ async def test_flow_reauth_no_username_or_device( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value=get_devices, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value=get_me, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "1234567890", - }, - ) - await hass.async_block_till_done() + mock_client.async_get_devices.return_value = get_devices + mock_client.async_get_me.return_value = get_me - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": p_error} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": p_error} + + mock_client.async_get_devices.return_value = get_data[2] + mock_client.async_get_me.return_value = get_data[1] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == {CONF_API_KEY: "1234567890"} -async def test_reconfigure_flow(hass: HomeAssistant) -> None: +async def test_reconfigure_flow(hass: HomeAssistant, mock_client: MagicMock) -> None: """Test a reconfigure flow.""" entry = MockConfigEntry( version=2, domain=DOMAIN, - unique_id="username", - data={"api_key": "1234567890"}, + unique_id="firstnamelastname", + data={CONF_API_KEY: "1234567890"}, ) entry.add_to_hass(hass) @@ -365,94 +330,61 @@ async def test_reconfigure_flow(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ) as mock_sensibo, - patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "1234567891"}, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reconfigure_successful" - assert entry.data == {"api_key": "1234567891"} - - assert len(mock_sensibo.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == {CONF_API_KEY: "1234567890"} @pytest.mark.parametrize( ("sideeffect", "p_error"), [ - (aiohttp.ClientConnectionError, "cannot_connect"), - (TimeoutError, "cannot_connect"), (AuthenticationError, "invalid_auth"), (SensiboError, "cannot_connect"), ], ) async def test_reconfigure_flow_error( - hass: HomeAssistant, sideeffect: Exception, p_error: str + hass: HomeAssistant, + sideeffect: Exception, + p_error: str, + mock_client: MagicMock, ) -> None: """Test a reconfigure flow with error.""" entry = MockConfigEntry( version=2, domain=DOMAIN, - unique_id="username", - data={"api_key": "1234567890"}, + unique_id="firstnamelastname", + data={CONF_API_KEY: "1234567890"}, ) entry.add_to_hass(hass) result = await entry.start_reconfigure_flow(hass) - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - side_effect=sideeffect, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "1234567890"}, - ) - await hass.async_block_till_done() + mock_client.async_get_devices.side_effect = sideeffect - assert result2["step_id"] == "reconfigure" - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": p_error} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_API_KEY: "1234567891"}, - ) - await hass.async_block_till_done() + assert result["step_id"] == "reconfigure" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": p_error} - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reconfigure_successful" - assert entry.data == {"api_key": "1234567891"} + mock_client.async_get_devices.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == {CONF_API_KEY: "1234567890"} @pytest.mark.parametrize( @@ -465,12 +397,12 @@ async def test_reconfigure_flow_error( ), ( {"result": []}, - {"result": {"username": "username"}}, + {"result": {"username": "firstnamelastname"}}, "no_devices", ), ( {"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - {"result": {"username": "username2"}}, + {"result": {"username": "firstnamelastname2"}}, "incorrect_api_key", ), ], @@ -480,13 +412,15 @@ async def test_flow_reconfigure_no_username_or_device( get_devices: dict[str, Any], get_me: dict[str, Any], p_error: str, + mock_client: MagicMock, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], ) -> None: - """Test config flow get no username from api.""" + """Test reconfigure flow with errors from api.""" entry = MockConfigEntry( version=2, domain=DOMAIN, - unique_id="username", - data={"api_key": "1234567890"}, + unique_id="firstnamelastname", + data={CONF_API_KEY: "1234567890"}, ) entry.add_to_hass(hass) @@ -495,24 +429,28 @@ async def test_flow_reconfigure_no_username_or_device( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value=get_devices, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value=get_me, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_API_KEY: "1234567890", - }, - ) - await hass.async_block_till_done() + mock_client.async_get_devices.return_value = get_devices + mock_client.async_get_me.return_value = get_me - assert result2["step_id"] == "reconfigure" - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": p_error} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result["step_id"] == "reconfigure" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": p_error} + + mock_client.async_get_devices.return_value = get_data[2] + mock_client.async_get_me.return_value = get_data[1] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == {CONF_API_KEY: "1234567890"} diff --git a/tests/components/sensibo/test_coordinator.py b/tests/components/sensibo/test_coordinator.py index d81b7fd613c..6cb8e6fe923 100644 --- a/tests/components/sensibo/test_coordinator.py +++ b/tests/components/sensibo/test_coordinator.py @@ -3,17 +3,17 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from typing import Any +from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory from pysensibo.exceptions import AuthenticationError, SensiboError from pysensibo.model import SensiboData -import pytest +from homeassistant.components.climate import HVACMode from homeassistant.components.sensibo.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util from . import ENTRY_CONFIG @@ -21,75 +21,69 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_coordinator( - hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData + hass: HomeAssistant, + mock_client: MagicMock, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo coordinator with errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="username", + unique_id="firstnamelastname", version=2, ) config_entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - ) as mock_data, - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - ): - monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) - mock_data.return_value = get_data - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("climate.hallway") - assert state.state == "heat" - mock_data.reset_mock() + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].hvac_mode = "heat" + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].device_on = True - mock_data.side_effect = SensiboError("info") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("climate.hallway") - assert state.state == STATE_UNAVAILABLE - mock_data.reset_mock() + mock_data = mock_client.async_get_devices_data + mock_data.return_value = get_data[0] + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT + mock_data.reset_mock() - mock_data.return_value = SensiboData(raw={}, parsed={}) - mock_data.side_effect = None - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=3)) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("climate.hallway") - assert state.state == STATE_UNAVAILABLE - mock_data.reset_mock() + mock_data.side_effect = SensiboError("info") + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() - monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) + mock_data.return_value = SensiboData(raw={}, parsed={}) + mock_data.side_effect = None + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() - mock_data.return_value = get_data - mock_data.side_effect = None - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("climate.hallway") - assert state.state == "heat" - mock_data.reset_mock() + mock_data.return_value = get_data[0] + mock_data.side_effect = None + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == HVACMode.HEAT + mock_data.reset_mock() - mock_data.side_effect = AuthenticationError("info") - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=7)) - await hass.async_block_till_done() - mock_data.assert_called_once() - state = hass.states.get("climate.hallway") - assert state.state == STATE_UNAVAILABLE + mock_data.side_effect = AuthenticationError("info") + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py index e17877b63b1..b8818149510 100644 --- a/tests/components/sensibo/test_entity.py +++ b/tests/components/sensibo/test_entity.py @@ -2,10 +2,10 @@ from __future__ import annotations -from unittest.mock import patch +from unittest.mock import MagicMock -from pysensibo.model import SensiboData import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_FAN_MODE, @@ -17,35 +17,24 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr -async def test_entity( +async def test_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, load_int: ConfigEntry, - get_data: SensiboData, + snapshot: SnapshotAssertion, ) -> None: - """Test the Sensibo climate.""" + """Test the Sensibo device.""" state1 = hass.states.get("climate.hallway") assert state1 - dr_entries = dr.async_entries_for_config_entry(device_registry, load_int.entry_id) - dr_entry: dr.DeviceEntry - for dr_entry in dr_entries: - if dr_entry.name == "Hallway": - assert dr_entry.identifiers == {("sensibo", "ABC999111")} - device_id = dr_entry.id - - er_entries = er.async_entries_for_device( - entity_registry, device_id, include_disabled_entities=True + assert ( + dr.async_entries_for_config_entry(device_registry, load_int.entry_id) + == snapshot ) - er_entry: er.RegistryEntry - for er_entry in er_entries: - if er_entry.name == "Hallway": - assert er_entry.unique_id == "Hallway" @pytest.mark.parametrize("p_error", SENSIBO_ERRORS) @@ -53,35 +42,30 @@ async def test_entity_failed_service_calls( hass: HomeAssistant, p_error: Exception, load_int: ConfigEntry, - get_data: SensiboData, + mock_client: MagicMock, ) -> None: """Test the Sensibo send command with error.""" state = hass.states.get("climate.hallway") assert state - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) state = hass.states.get("climate.hallway") assert state.attributes["fan_mode"] == "low" - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - side_effect=p_error, - ), - pytest.raises(HomeAssistantError), - ): + mock_client.async_set_ac_state_property.side_effect = p_error + + with pytest.raises(HomeAssistantError): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py index 2938d4ede0e..b4911983fe7 100644 --- a/tests/components/sensibo/test_init.py +++ b/tests/components/sensibo/test_init.py @@ -1,10 +1,14 @@ -"""Test for Sensibo component Init.""" +"""Test for Sensibo integration setup.""" from __future__ import annotations -from unittest.mock import patch +from datetime import timedelta +from typing import Any +from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory from pysensibo.model import SensiboData +import pytest from homeassistant.components.sensibo.const import DOMAIN from homeassistant.components.sensibo.util import NoUsernameError @@ -15,141 +19,72 @@ from homeassistant.setup import async_setup_component from . import ENTRY_CONFIG -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import WebSocketGenerator -async def test_setup_entry(hass: HomeAssistant, get_data: SensiboData) -> None: - """Test setup entry.""" +async def test_load_unload_entry(hass: HomeAssistant, mock_client: MagicMock) -> None: + """Test setup and unload config entry.""" entry = MockConfigEntry( domain=DOMAIN, - source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="12", + unique_id="firstnamelastname", version=2, ) entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - ): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() -async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None: + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_migrate_entry(hass: HomeAssistant, mock_client: MagicMock) -> None: """Test migrate entry unique id.""" entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="12", + unique_id="someother", version=1, ) entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - ): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED assert entry.version == 2 - assert entry.unique_id == "username" + assert entry.unique_id == "firstnamelastname" -async def test_migrate_entry_fails(hass: HomeAssistant, get_data: SensiboData) -> None: - """Test migrate entry unique id.""" +async def test_migrate_entry_fails(hass: HomeAssistant, mock_client: MagicMock) -> None: + """Test migrate entry fails.""" entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, data=ENTRY_CONFIG, entry_id="1", - unique_id="12", + unique_id="someother", version=1, ) entry.add_to_hass(hass) - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - side_effect=NoUsernameError("No username returned"), - ), - ): - await hass.config_entries.async_setup(entry.entry_id) + mock_client.async_get_me.side_effect = NoUsernameError("No username returned") + + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.MIGRATION_ERROR assert entry.version == 1 - assert entry.unique_id == "12" - - -async def test_unload_entry(hass: HomeAssistant, get_data: SensiboData) -> None: - """Test unload an entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data=ENTRY_CONFIG, - entry_id="1", - unique_id="12", - version="2", - ) - entry.add_to_hass(hass) - - with ( - patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is ConfigEntryState.NOT_LOADED + assert entry.unique_id == "someother" async def test_device_remove_devices( @@ -174,3 +109,73 @@ async def test_device_remove_devices( ) response = await client.remove_device(dead_device_entry.id, load_int.entry_id) assert response["success"] + + +@pytest.mark.parametrize( + ("entity_id", "device_ids"), + [ + # Device is ABC999111 + ("climate.hallway", ["ABC999111"]), + ("binary_sensor.hallway_filter_clean_required", ["ABC999111"]), + ("number.hallway_temperature_calibration", ["ABC999111"]), + ("sensor.hallway_filter_last_reset", ["ABC999111"]), + ("update.hallway_firmware", ["ABC999111"]), + # Device is AABBCC belonging to device ABC999111 + ("binary_sensor.hallway_motion_sensor_motion", ["ABC999111", "AABBCC"]), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_automatic_device_addition_and_removal( + hass: HomeAssistant, + load_int: ConfigEntry, + mock_client: MagicMock, + get_data: tuple[SensiboData, dict[str, Any], dict[str, Any]], + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, + entity_id: str, + device_ids: list[str], +) -> None: + """Test for automatic device addition and removal.""" + + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + for device_id in device_ids: + assert device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + + # Remove one of the devices + new_device_list = [ + device for device in get_data[2]["result"] if device["id"] != device_ids[0] + ] + mock_client.async_get_devices.return_value = { + "status": "success", + "result": new_device_list, + } + new_data = {k: v for k, v in get_data[0].parsed.items() if k != device_ids[0]} + new_raw = mock_client.async_get_devices.return_value["result"] + mock_client.async_get_devices_data.return_value = SensiboData(new_raw, new_data) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert not state + assert not entity_registry.async_get(entity_id) + for device_id in device_ids: + assert not device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + + # Add the device back + mock_client.async_get_devices.return_value = get_data[2] + mock_client.async_get_devices_data.return_value = get_data[0] + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + for device_id in device_ids: + assert device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) diff --git a/tests/components/sensibo/test_number.py b/tests/components/sensibo/test_number.py index 95836ba023c..ff5347a02d3 100644 --- a/tests/components/sensibo/test_number.py +++ b/tests/components/sensibo/test_number.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock -from pysensibo.model import SensiboData +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -19,7 +19,6 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, snapshot_platform @@ -32,81 +31,57 @@ from tests.common import async_fire_time_changed, snapshot_platform async def test_number( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo number.""" await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) - monkeypatch.setattr(get_data.parsed["ABC999111"], "calibration_temp", 0.2) + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].calibration_temp = 0.2 - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state1 = hass.states.get("number.hallway_temperature_calibration") - assert state1.state == "0.2" + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.2" @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_number_set_value( - hass: HomeAssistant, - load_int: ConfigEntry, - get_data: SensiboData, + hass: HomeAssistant, load_int: ConfigEntry, mock_client: MagicMock ) -> None: """Test the Sensibo number service.""" - state1 = hass.states.get("number.hallway_temperature_calibration") - assert state1.state == "0.1" + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.1" - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", - return_value={"status": "failure"}, - ), - ): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - NUMBER_DOMAIN, - SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_VALUE: "0.2"}, - blocking=True, - ) - await hass.async_block_till_done() + mock_client.async_set_calibration.return_value = {"status": "failure"} - state2 = hass.states.get("number.hallway_temperature_calibration") - assert state2.state == "0.1" - - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", - return_value={"status": "success"}, - ), - ): + with pytest.raises(HomeAssistantError): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_VALUE: "0.2"}, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: "0.2"}, blocking=True, ) - await hass.async_block_till_done() - state2 = hass.states.get("number.hallway_temperature_calibration") - assert state2.state == "0.2" + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.1" + + mock_client.async_set_calibration.return_value = {"status": "success"} + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: "0.2"}, + blocking=True, + ) + + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.2" diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index 2e4a1cb507c..75dbdc88840 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from pysensibo.model import SensiboData +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -14,14 +14,16 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) +from homeassistant.components.sensibo.const import DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util +from homeassistant.helpers import entity_registry as er, issue_registry as ir -from tests.common import async_fire_time_changed, snapshot_platform +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.parametrize( @@ -31,155 +33,208 @@ from tests.common import async_fire_time_changed, snapshot_platform async def test_select( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo select.""" await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) - monkeypatch.setattr( - get_data.parsed["ABC999111"], "horizontal_swing_mode", "fixedleft" - ) + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].light_mode = "dim" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state1 = hass.states.get("select.hallway_horizontal_swing") - assert state1.state == "fixedleft" + state = hass.states.get("select.kitchen_light") + assert state.state == "dim" async def test_select_set_option( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo select service.""" - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "targetTemperature", - "light", - ], + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].active_features = [ + "timestamp", + "on", + "mode", + "targetTemperature", + "light", + ] + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("select.kitchen_light") + assert state.state == "on" + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "failed"} + } + + with pytest.raises( + HomeAssistantError, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: "dim"}, + blocking=True, + ) + + state = hass.states.get("select.kitchen_light") + assert state.state == "on" + + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].active_features = [ + "timestamp", + "on", + "mode", + "light", + ] + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Failed", "failureReason": "No connection"} + } + + with pytest.raises( + HomeAssistantError, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: "dim"}, + blocking=True, + ) + + state = hass.states.get("select.kitchen_light") + assert state.state == "on" + + mock_client.async_set_ac_state_property.return_value = { + "result": {"status": "Success"} + } + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state.entity_id, ATTR_OPTION: "dim"}, + blocking=True, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + state = hass.states.get("select.kitchen_light") + assert state.state == "dim" - state1 = hass.states.get("select.hallway_horizontal_swing") - assert state1.state == "stopped" + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].active_features = [ + "timestamp", + "on", + "mode", + ] - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "failed"}}, - ), - pytest.raises( - HomeAssistantError, - ), - ): - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, - blocking=True, - ) + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - state2 = hass.states.get("select.hallway_horizontal_swing") - assert state2.state == "stopped" + state = hass.states.get("select.kitchen_light") + assert state.state == STATE_UNAVAILABLE - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "active_features", - [ - "timestamp", - "on", - "mode", - "targetTemperature", - "horizontalSwing", - "light", - ], + +@pytest.mark.parametrize( + "load_platforms", + [[Platform.SELECT]], +) +async def test_deprecated_horizontal_swing_select( + hass: HomeAssistant, + load_platforms: list[Platform], + mock_client: MagicMock, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test the deprecated horizontal swing select entity.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="firstnamelastname", + version=2, ) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) + config_entry.add_to_hass(hass) + + entity_registry.async_get_or_create( + SELECT_DOMAIN, + DOMAIN, + "ABC999111-horizontalSwing", + config_entry=config_entry, + disabled_by=None, + has_entity_name=True, + suggested_object_id="hallway_horizontal_swing", + ) + + with patch("homeassistant.components.sensibo.PLATFORMS", load_platforms): + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={ - "result": {"status": "Failed", "failureReason": "No connection"} - }, - ), - pytest.raises( - HomeAssistantError, - ), - ): - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, - blocking=True, - ) - await hass.async_block_till_done() + state = hass.states.get("select.hallway_horizontal_swing") + assert state.state == "stopped" - state2 = hass.states.get("select.hallway_horizontal_swing") - assert state2.state == "stopped" + # No issue created without automation or script + assert issue_registry.issues == {} with ( + patch("homeassistant.components.sensibo.PLATFORMS", load_platforms), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + # Patch check for automation, that one exist + "homeassistant.components.sensibo.select.automations_with_entity", + return_value=["automation.test"], ), ): - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, - blocking=True, - ) - await hass.async_block_till_done() + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done(True) - state2 = hass.states.get("select.hallway_horizontal_swing") - assert state2.state == "fixedleft" + # Issue is created when entity is enabled and automation/script exist + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_entity_horizontalswing") + assert issue + assert issue.translation_key == "deprecated_entity_horizontalswing" + assert hass.states.get("select.hallway_horizontal_swing") + assert entity_registry.async_is_registered("select.hallway_horizontal_swing") + + # Disabling the entity should remove the entity and remove the issue + # once the integration is reloaded + entity_registry.async_update_entity( + state.entity_id, disabled_by=er.RegistryEntryDisabler.USER + ) + + with ( + patch("homeassistant.components.sensibo.PLATFORMS", load_platforms), + patch( + "homeassistant.components.sensibo.select.automations_with_entity", + return_value=["automation.test"], + ), + ): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done(True) + + # Disabling the entity and reloading has removed the entity and issue + assert not hass.states.get("select.hallway_horizontal_swing") + assert not entity_registry.async_is_registered("select.hallway_horizontal_swing") + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_entity_horizontalswing") + assert not issue diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index 32794e266b0..8ea76036123 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -1,12 +1,12 @@ -"""The test for the sensibo select platform.""" +"""The test for the sensibo sensor platform.""" from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory -from pysensibo.model import PureAQI, SensiboData +from pysensibo.model import PureAQI import pytest from syrupy.assertion import SnapshotAssertion @@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, snapshot_platform @@ -27,8 +26,7 @@ from tests.common import async_fire_time_changed, snapshot_platform async def test_sensor( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, freezer: FrozenDateTimeFactory, @@ -37,17 +35,13 @@ async def test_sensor( await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pm25_pure", PureAQI(2)) + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pm25_pure = PureAQI(2) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state1 = hass.states.get("sensor.kitchen_pure_aqi") - assert state1.state == "moderate" + state = hass.states.get("sensor.kitchen_pure_aqi") + assert state.state == "moderate" diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py index f260af7baaa..43d87c31535 100644 --- a/tests/components/sensibo/test_switch.py +++ b/tests/components/sensibo/test_switch.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock -from pysensibo.model import SensiboData +from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -22,7 +22,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, snapshot_platform @@ -45,221 +44,159 @@ async def test_switch( async def test_switch_timer( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the Sensibo switch.""" + """Test the Sensibo switch timer.""" - state1 = hass.states.get("switch.hallway_timer") - assert state1.state == STATE_OFF - assert state1.attributes["id"] is None - assert state1.attributes["turn_on"] is None + state = hass.states.get("switch.hallway_timer") + assert state.state == STATE_OFF + assert state.attributes["id"] is None + assert state.attributes["turn_on"] is None - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, - ), - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: state1.entity_id, - }, - blocking=True, - ) + mock_client.async_set_timer.return_value = { + "status": "success", + "result": {"id": "SzTGE4oZ4D"}, + } + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].timer_on = True + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].timer_id = "SzTGE4oZ4D" + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].timer_state_on = False + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", True) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", "SzTGE4oZ4D") - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", False) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - state1 = hass.states.get("switch.hallway_timer") - assert state1.state == STATE_ON - assert state1.attributes["id"] == "SzTGE4oZ4D" - assert state1.attributes["turn_on"] is False + state = hass.states.get("switch.hallway_timer") + assert state.state == STATE_ON + assert state.attributes["id"] == "SzTGE4oZ4D" + assert state.attributes["turn_on"] is False - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", - return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, - ), - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: state1.entity_id, - }, - blocking=True, - ) + mock_client.async_del_timer.return_value = { + "status": "success", + "result": {"id": "SzTGE4oZ4D"}, + } + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].timer_on = False + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", False) - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state1 = hass.states.get("switch.hallway_timer") - assert state1.state == STATE_OFF + state = hass.states.get("switch.hallway_timer") + assert state.state == STATE_OFF async def test_switch_pure_boost( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the Sensibo switch.""" + """Test the Sensibo switch pure boost.""" - state1 = hass.states.get("switch.kitchen_pure_boost") - assert state1.state == STATE_OFF + state = hass.states.get("switch.kitchen_pure_boost") + assert state.state == STATE_OFF - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - return_value={"status": "success"}, - ), - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: state1.entity_id, - }, - blocking=True, - ) + mock_client.async_set_pureboost.return_value = {"status": "success"} + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_boost_enabled = True + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_measure_integration = None + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True) - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None) + state = hass.states.get("switch.kitchen_pure_boost") + assert state.state == STATE_ON - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - state1 = hass.states.get("switch.kitchen_pure_boost") - assert state1.state == STATE_ON + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - return_value={"status": "success"}, - ), - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: state1.entity_id, - }, - blocking=True, - ) + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pure_boost_enabled = False + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", False) - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state1 = hass.states.get("switch.kitchen_pure_boost") - assert state1.state == STATE_OFF + state = hass.states.get("switch.kitchen_pure_boost") + assert state.state == STATE_OFF async def test_switch_command_failure( - hass: HomeAssistant, - load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + hass: HomeAssistant, load_int: ConfigEntry, mock_client: MagicMock ) -> None: """Test the Sensibo switch fails commands.""" - state1 = hass.states.get("switch.hallway_timer") + state = hass.states.get("switch.hallway_timer") - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ), - pytest.raises( - HomeAssistantError, - ), + mock_client.async_set_timer.return_value = {"status": "failure"} + + with pytest.raises( + HomeAssistantError, ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: state1.entity_id, + ATTR_ENTITY_ID: state.entity_id, }, blocking=True, ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", - return_value={"status": "failure"}, - ), - pytest.raises( - HomeAssistantError, - ), + mock_client.async_del_timer.return_value = {"status": "failure"} + + with pytest.raises( + HomeAssistantError, ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, { - ATTR_ENTITY_ID: state1.entity_id, + ATTR_ENTITY_ID: state.entity_id, }, blocking=True, ) @@ -268,114 +205,78 @@ async def test_switch_command_failure( async def test_switch_climate_react( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Sensibo switch for climate react.""" - state1 = hass.states.get("switch.hallway_climate_react") - assert state1.state == STATE_OFF + state = hass.states.get("switch.hallway_climate_react") + assert state.state == STATE_OFF - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react", - return_value={"status": "success"}, - ), - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: state1.entity_id, - }, - blocking=True, - ) + mock_client.async_enable_climate_react.return_value = {"status": "success"} + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) + + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].smart_on = True + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", True) + state = hass.states.get("switch.hallway_climate_react") + assert state.state == STATE_ON - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - state1 = hass.states.get("switch.hallway_climate_react") - assert state1.state == STATE_ON + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: state.entity_id, + }, + blocking=True, + ) - with ( - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), - patch( - "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react", - return_value={"status": "success"}, - ), - ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: state1.entity_id, - }, - blocking=True, - ) + mock_client.async_get_devices_data.return_value.parsed["ABC999111"].smart_on = False + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_on", False) - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state1 = hass.states.get("switch.hallway_climate_react") - assert state1.state == STATE_OFF + state = hass.states.get("switch.hallway_climate_react") + assert state.state == STATE_OFF async def test_switch_climate_react_no_data( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, + freezer: FrozenDateTimeFactory, ) -> None: - """Test the Sensibo switch for climate react.""" + """Test the Sensibo switch for climate react with no data.""" - monkeypatch.setattr(get_data.parsed["ABC999111"], "smart_type", None) + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].smart_type = None - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt_util.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state1 = hass.states.get("switch.hallway_climate_react") - assert state1.state == STATE_OFF + state = hass.states.get("switch.hallway_climate_react") + assert state.state == STATE_OFF with pytest.raises(HomeAssistantError): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: state1.entity_id, + ATTR_ENTITY_ID: state.entity_id, }, blocking=True, ) - await hass.async_block_till_done() diff --git a/tests/components/sensibo/test_update.py b/tests/components/sensibo/test_update.py index a4eb9751243..08ee68fdb8e 100644 --- a/tests/components/sensibo/test_update.py +++ b/tests/components/sensibo/test_update.py @@ -3,10 +3,9 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory -from pysensibo.model import SensiboData import pytest from syrupy.assertion import SnapshotAssertion @@ -26,8 +25,7 @@ from tests.common import async_fire_time_changed, snapshot_platform async def test_update( hass: HomeAssistant, load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, + mock_client: MagicMock, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, freezer: FrozenDateTimeFactory, @@ -36,15 +34,13 @@ async def test_update( await snapshot_platform(hass, entity_registry, snapshot, load_int.entry_id) - monkeypatch.setattr(get_data.parsed["ABC999111"], "fw_ver", "SKY30048") + mock_client.async_get_devices_data.return_value.parsed[ + "ABC999111" + ].fw_ver = "SKY30048" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - freezer.tick(timedelta(minutes=5)) - async_fire_time_changed(hass) - await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - state1 = hass.states.get("update.hallway_firmware") - assert state1.state == STATE_OFF + state = hass.states.get("update.hallway_firmware") + assert state.state == STATE_OFF diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index f50e92bc9df..f35c9520f71 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.json import load_json from .common import UNITS_OF_MEASUREMENT, MockSensor diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0ea46a41273..b162200f95e 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -45,7 +45,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -570,7 +570,7 @@ async def test_unit_translation_key_without_platform_raises( match="cannot have a translation key for unit of measurement before " "being added to the entity platform", ): - unit = entity0.unit_of_measurement # noqa: F841 + unit = entity0.unit_of_measurement setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) @@ -580,7 +580,7 @@ async def test_unit_translation_key_without_platform_raises( await hass.async_block_till_done() # Should not raise after being added to the platform - unit = entity0.unit_of_measurement # noqa: F841 + unit = entity0.unit_of_measurement assert unit == "Tests" @@ -2144,7 +2144,7 @@ async def test_non_numeric_validation_raise( (13, "13"), (17.50, "17.5"), ("1e-05", "1e-05"), - (Decimal(18.50), "18.5"), + (Decimal("18.50"), "18.50"), ("19.70", "19.70"), (None, STATE_UNKNOWN), ], @@ -2584,7 +2584,7 @@ async def test_name(hass: HomeAssistant) -> None: async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test sensor platform via config entry.""" async_add_entities([entity1, entity2, entity3, entity4]) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 44eaa9fde0d..615960defbb 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -40,7 +40,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from .common import MockSensor @@ -57,7 +57,7 @@ from tests.components.recorder.common import ( ) from tests.typing import ( MockHAClientWebSocket, - RecorderInstanceGenerator, + RecorderInstanceContextManager, WebSocketGenerator, ) @@ -102,7 +102,7 @@ KW_SENSOR_ATTRIBUTES = { @pytest.fixture async def mock_recorder_before_hass( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> None: """Set up recorder patches.""" @@ -121,15 +121,6 @@ def disable_mariadb_issue() -> None: yield -@pytest.fixture(autouse=True) -def disable_sqlite_issue() -> None: - """Disable creating issue about outdated SQLite version.""" - with patch( - "homeassistant.components.recorder.util._async_create_issue_deprecated_version" - ): - yield - - async def async_list_statistic_ids( hass: HomeAssistant, statistic_ids: set[str] | None = None, @@ -4247,8 +4238,14 @@ async def async_record_states( @pytest.mark.parametrize( ("units", "attributes", "unit", "unit2", "supported_unit"), [ - (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "GW, MW, TW, W, kW"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "GW, MW, TW, W, kW"), + ( + US_CUSTOMARY_SYSTEM, + POWER_SENSOR_ATTRIBUTES, + "W", + "kW", + "GW, MW, TW, W, kW, mW", + ), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "GW, MW, TW, W, kW, mW"), ( US_CUSTOMARY_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, @@ -4459,8 +4456,14 @@ async def test_validate_statistics_unit_ignore_device_class( @pytest.mark.parametrize( ("units", "attributes", "unit", "unit2", "supported_unit"), [ - (US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "GW, MW, TW, W, kW"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "GW, MW, TW, W, kW"), + ( + US_CUSTOMARY_SYSTEM, + POWER_SENSOR_ATTRIBUTES, + "W", + "kW", + "GW, MW, TW, W, kW, mW", + ), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "kW", "GW, MW, TW, W, kW, mW"), ( US_CUSTOMARY_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, diff --git a/tests/components/sensor/test_recorder_missing_stats.py b/tests/components/sensor/test_recorder_missing_stats.py index 43e18b89e72..fd28a7052a5 100644 --- a/tests/components/sensor/test_recorder_missing_stats.py +++ b/tests/components/sensor/test_recorder_missing_stats.py @@ -17,14 +17,14 @@ from homeassistant.components.recorder.util import session_scope from homeassistant.core import CoreState from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_test_home_assistant from tests.components.recorder.common import ( async_wait_recording_done, do_adhoc_statistics, ) -from tests.typing import RecorderInstanceGenerator +from tests.typing import RecorderInstanceContextManager POWER_SENSOR_ATTRIBUTES = { "device_class": "energy", @@ -47,7 +47,7 @@ def disable_db_issue_creation(): @pytest.mark.parametrize("enable_missing_statistics", [True]) @pytest.mark.usefixtures("hass_storage") # Prevent test hass from writing to storage async def test_compile_missing_statistics( - async_test_recorder: RecorderInstanceGenerator, freezer: FrozenDateTimeFactory + async_test_recorder: RecorderInstanceContextManager, freezer: FrozenDateTimeFactory ) -> None: """Test compile missing statistics.""" three_days_ago = datetime(2021, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index 7e87dd1c6b8..194f4fc4a78 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -79,6 +79,38 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=HTW_SERVICE_INFO.address, + source=config_entries.SOURCE_IGNORE, + data={}, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/seventeentrack/test_repairs.py b/tests/components/seventeentrack/test_repairs.py deleted file mode 100644 index 44d1f078432..00000000000 --- a/tests/components/seventeentrack/test_repairs.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Tests for the seventeentrack repair flow.""" - -from unittest.mock import AsyncMock - -from freezegun.api import FrozenDateTimeFactory - -from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN -from homeassistant.components.seventeentrack import DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component - -from . import goto_future, init_integration -from .conftest import DEFAULT_SUMMARY_LENGTH, get_package - -from tests.common import MockConfigEntry -from tests.components.repairs import process_repair_fix_flow, start_repair_fix_flow -from tests.typing import ClientSessionGenerator - - -async def test_repair( - hass: HomeAssistant, - mock_seventeentrack: AsyncMock, - issue_registry: ir.IssueRegistry, - hass_client: ClientSessionGenerator, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, -) -> None: - """Ensure everything starts correctly.""" - await init_integration(hass, mock_config_entry) # 2 - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH - assert len(issue_registry.issues) == 1 - - package = get_package() - mock_seventeentrack.return_value.profile.packages.return_value = [package] - await goto_future(hass, freezer) - - assert hass.states.get("sensor.17track_package_friendly_name_1") - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - - assert "deprecated" not in mock_config_entry.data - - repair_issue = issue_registry.async_get_issue( - domain=DOMAIN, issue_id=f"deprecate_sensor_{mock_config_entry.entry_id}" - ) - - assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) - - client = await hass_client() - - data = await start_repair_fix_flow(client, DOMAIN, repair_issue.issue_id) - - flow_id = data["flow_id"] - assert data == { - "type": "form", - "flow_id": flow_id, - "handler": DOMAIN, - "step_id": "confirm", - "data_schema": [], - "errors": None, - "description_placeholders": None, - "last_step": None, - "preview": None, - } - - data = await process_repair_fix_flow(client, flow_id) - - flow_id = data["flow_id"] - assert data == { - "type": "create_entry", - "handler": DOMAIN, - "flow_id": flow_id, - "description": None, - "description_placeholders": None, - } - - assert mock_config_entry.data["deprecated"] - - repair_issue = issue_registry.async_get_issue( - domain=DOMAIN, issue_id="deprecate_sensor" - ) - - assert repair_issue is None - - await goto_future(hass, freezer) - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index a631996b4eb..5367fabba9e 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from pyseventeentrack.errors import SeventeenTrackError @@ -63,87 +63,6 @@ async def test_login_exception( assert not hass.states.async_entity_ids("sensor") -async def test_add_package( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_seventeentrack: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure package is added correctly when user add a new package.""" - package = get_package() - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await init_integration(hass, mock_config_entry) - assert hass.states.get("sensor.17track_package_friendly_name_1") - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - - package2 = get_package( - tracking_number="789", - friendly_name="friendly name 2", - info_text="info text 2", - location="location 2", - timestamp="2020-08-10 14:25", - ) - mock_seventeentrack.return_value.profile.packages.return_value = [package, package2] - - await goto_future(hass, freezer) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 - - -async def test_add_package_default_friendly_name( - hass: HomeAssistant, - mock_seventeentrack: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure package is added correctly with default friendly name when user add a new package without his own friendly name.""" - package = get_package(friendly_name=None) - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await init_integration(hass, mock_config_entry) - state_456 = hass.states.get("sensor.17track_package_456") - assert state_456 is not None - assert state_456.attributes["friendly_name"] == "17Track Package 456" - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - - -async def test_remove_package( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_seventeentrack: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure entity is not there anymore if package is not there.""" - package1 = get_package() - package2 = get_package( - tracking_number="789", - friendly_name="friendly name 2", - info_text="info text 2", - location="location 2", - timestamp="2020-08-10 14:25", - ) - - mock_seventeentrack.return_value.profile.packages.return_value = [ - package1, - package2, - ] - - await init_integration(hass, mock_config_entry) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is not None - assert hass.states.get("sensor.17track_package_friendly_name_2") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 2 - - mock_seventeentrack.return_value.profile.packages.return_value = [package2] - - await goto_future(hass, freezer) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is None - assert hass.states.get("sensor.17track_package_friendly_name_2") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - - async def test_package_error( hass: HomeAssistant, mock_seventeentrack: AsyncMock, @@ -159,72 +78,6 @@ async def test_package_error( assert hass.states.get("sensor.17track_package_friendly_name_1") is None -async def test_delivered_not_shown( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_seventeentrack: AsyncMock, - mock_config_entry_with_default_options: MockConfigEntry, -) -> None: - """Ensure delivered packages are not shown.""" - package = get_package(status=40) - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - with patch( - "homeassistant.components.seventeentrack.sensor.persistent_notification" - ) as persistent_notification_mock: - await init_integration(hass, mock_config_entry_with_default_options) - await goto_future(hass, freezer) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is None - persistent_notification_mock.create.assert_called() - - -async def test_delivered_shown( - hass: HomeAssistant, - mock_seventeentrack: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure delivered packages are show when user choose to show them.""" - package = get_package(status=40) - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - with patch( - "homeassistant.components.seventeentrack.sensor.persistent_notification" - ) as persistent_notification_mock: - await init_integration(hass, mock_config_entry) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - persistent_notification_mock.create.assert_not_called() - - -async def test_becomes_delivered_not_shown_notification( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_seventeentrack: AsyncMock, - mock_config_entry_with_default_options: MockConfigEntry, -) -> None: - """Ensure notification is triggered when package becomes delivered.""" - package = get_package() - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await init_integration(hass, mock_config_entry_with_default_options) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - - package_delivered = get_package(status=40) - mock_seventeentrack.return_value.profile.packages.return_value = [package_delivered] - - with patch( - "homeassistant.components.seventeentrack.sensor.persistent_notification" - ) as persistent_notification_mock: - await goto_future(hass, freezer) - - persistent_notification_mock.create.assert_called() - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH - - async def test_summary_correctly_updated( hass: HomeAssistant, freezer: FrozenDateTimeFactory, @@ -237,7 +90,7 @@ async def test_summary_correctly_updated( await init_integration(hass, mock_config_entry) - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 + assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH state_ready_picked = hass.states.get("sensor.17track_ready_to_be_picked_up") assert state_ready_picked is not None @@ -278,25 +131,6 @@ async def test_summary_error( ) -async def test_utc_timestamp( - hass: HomeAssistant, - mock_seventeentrack: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Ensure package timestamp is converted correctly from HA-defined time zone to UTC.""" - - package = get_package(tz="Asia/Jakarta") - mock_seventeentrack.return_value.profile.packages.return_value = [package] - - await init_integration(hass, mock_config_entry) - - assert hass.states.get("sensor.17track_package_friendly_name_1") is not None - assert len(hass.states.async_entity_ids()) == DEFAULT_SUMMARY_LENGTH + 1 - state_456 = hass.states.get("sensor.17track_package_friendly_name_1") - assert state_456 is not None - assert str(state_456.attributes.get("timestamp")) == "2020-08-10 03:32:00+00:00" - - async def test_non_valid_platform_config( hass: HomeAssistant, mock_seventeentrack: AsyncMock ) -> None: diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 15308fad91f..4718abc02b5 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://192.168.0.1', 'connections': set({ }), @@ -41,6 +42,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -72,6 +74,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -132,6 +135,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://192.168.0.1', 'connections': set({ }), @@ -169,6 +173,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -200,6 +205,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 67b2198fd2b..68a1e7f7227 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://192.168.0.1', 'connections': set({ }), @@ -41,6 +42,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 7645a4ad8bf..6376ef24ce2 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://192.168.0.1', 'connections': set({ }), @@ -48,6 +49,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -79,6 +81,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -110,6 +113,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -149,6 +153,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -180,6 +185,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -211,6 +217,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -242,6 +249,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -275,6 +283,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -308,6 +317,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -341,6 +351,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -374,6 +385,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -407,6 +419,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -440,6 +453,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -480,6 +494,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -524,6 +539,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 7de45eeee98..7a20560e25f 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -148,6 +148,13 @@ def get_entity_state(hass: HomeAssistant, entity_id: str) -> str: return entity.state +def get_entity_attribute(hass: HomeAssistant, entity_id: str, attribute: str) -> str: + """Return entity attribute.""" + entity = hass.states.get(entity_id) + assert entity + return entity.attributes[attribute] + + def register_device( device_registry: DeviceRegistry, config_entry: ConfigEntry ) -> DeviceEntry: diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index d453d25698c..b3074742949 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -2,6 +2,15 @@ from unittest.mock import AsyncMock, Mock, PropertyMock, patch +from aioshelly.ble.const import ( + BLE_CODE, + BLE_SCAN_RESULT_EVENT, + BLE_SCAN_RESULT_VERSION, + BLE_SCRIPT_NAME, + VAR_ACTIVE, + VAR_EVENT_TYPE, + VAR_VERSION, +) from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.const import MODEL_1, MODEL_25, MODEL_PLUS_2PM from aioshelly.rpc_device import RpcDevice, RpcUpdateType @@ -180,6 +189,7 @@ MOCK_CONFIG = { "xcounts": {"expr": None, "unit": None}, "xfreq": {"expr": None, "unit": None}, }, + "flood:0": {"id": 0, "name": "Test name"}, "light:0": {"name": "test light_0"}, "light:1": {"name": "test light_1"}, "light:2": {"name": "test light_2"}, @@ -199,8 +209,72 @@ MOCK_CONFIG = { }, "wifi": {"sta": {"enable": True}, "sta1": {"enable": False}}, "ws": {"enable": False, "server": None}, + "voltmeter:100": {"xvoltage": {"unit": "ppm"}}, + "script:1": {"id": 1, "name": "test_script.js", "enable": True}, + "script:2": {"id": 2, "name": "test_script_2.js", "enable": False}, + "script:3": {"id": 3, "name": BLE_SCRIPT_NAME, "enable": False}, } + +MOCK_BLU_TRV_REMOTE_CONFIG = { + "components": [ + { + "key": "blutrv:200", + "status": { + "id": 200, + "target_C": 17.1, + "current_C": 17.1, + "pos": 0, + "rssi": -60, + "battery": 100, + "packet_id": 58, + "last_updated_ts": 1734967725, + "paired": True, + "rpc": True, + "rsv": 61, + }, + "config": { + "id": 200, + "addr": "f8:44:77:25:f0:dd", + "name": "TRV-Name", + "key": None, + "trv": "bthomedevice:200", + "temp_sensors": [], + "dw_sensors": [], + "override_delay": 30, + "meta": {}, + }, + }, + ], + "blutrv:200": { + "id": 0, + "enable": True, + "min_valve_position": 0, + "default_boost_duration": 1800, + "default_override_duration": 2147483647, + "default_override_target_C": 8, + "addr": "f8:44:77:25:f0:dd", + "name": "TRV-Name", + "local_name": "SBTR-001AEU", + }, +} + + +MOCK_BLU_TRV_REMOTE_STATUS = { + "blutrv:200": { + "id": 0, + "pos": 0, + "steps": 0, + "current_C": 15.2, + "target_C": 17.1, + "schedule_rev": 0, + "rssi": -60, + "battery": 100, + "errors": [], + }, +} + + MOCK_SHELLY_COAP = { "mac": MOCK_MAC, "auth": False, @@ -265,6 +339,7 @@ MOCK_STATUS_RPC = { "em1:1": {"act_power": 123.3}, "em1data:0": {"total_act_energy": 123456.4}, "em1data:1": {"total_act_energy": 987654.3}, + "flood:0": {"id": 0, "alarm": False, "mute": False}, "thermostat:0": { "id": 0, "enable": True, @@ -272,6 +347,15 @@ MOCK_STATUS_RPC = { "current_C": 12.3, "output": True, }, + "script:1": { + "id": 1, + "running": True, + "mem_used": 826, + "mem_peak": 1666, + "mem_free": 24360, + }, + "script:2": {"id": 2, "running": False}, + "script:3": {"id": 3, "running": False}, "humidity:0": {"rh": 44.4}, "sys": { "available_updates": { @@ -280,10 +364,32 @@ MOCK_STATUS_RPC = { }, "relay_in_thermostat": True, }, - "voltmeter": {"voltage": 4.321}, + "voltmeter:100": {"voltage": 4.321, "xvoltage": 12.34}, "wifi": {"rssi": -63}, } +MOCK_SCRIPTS = [ + """" +function eventHandler(event, userdata) { + if (typeof event.component !== "string") + return; + + let component = event.component.substring(0, 5); + if (component === "input") { + let id = Number(event.component.substring(6)); + Shelly.emitEvent("input_event", { id: id }); + } +} + +Shelly.addEventHandler(eventHandler); +Shelly.emitEvent("script_start"); +""", + 'console.log("Hello World!")', + BLE_CODE.replace(VAR_ACTIVE, "true") + .replace(VAR_EVENT_TYPE, BLE_SCAN_RESULT_EVENT) + .replace(VAR_VERSION, str(BLE_SCAN_RESULT_VERSION)), +] + @pytest.fixture(autouse=True) def mock_coap(): @@ -367,6 +473,27 @@ def _mock_rpc_device(version: str | None = None): firmware_version="some fw string", initialized=True, connected=True, + script_getcode=AsyncMock( + side_effect=lambda script_id: {"data": MOCK_SCRIPTS[script_id - 1]} + ), + ) + type(device).name = PropertyMock(return_value="Test name") + return device + + +def _mock_blu_rtv_device(version: str | None = None): + """Mock rpc (Gen2, Websocket) device.""" + device = Mock( + spec=RpcDevice, + config=MOCK_CONFIG | MOCK_BLU_TRV_REMOTE_CONFIG, + event={}, + shelly=MOCK_SHELLY_RPC, + version=version or "1.0.0", + hostname="test-host", + status=MOCK_STATUS_RPC | MOCK_BLU_TRV_REMOTE_STATUS, + firmware_version="some fw string", + initialized=True, + connected=True, ) type(device).name = PropertyMock(return_value="Test name") return device @@ -419,3 +546,24 @@ async def mock_rpc_device(): @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth: None) -> None: """Auto mock bluetooth.""" + + +@pytest.fixture(autouse=True) +async def mock_blu_trv(): + """Mock BLU TRV.""" + + with ( + patch("aioshelly.rpc_device.RpcDevice.create") as blu_trv_device_mock, + patch("homeassistant.components.shelly.bluetooth.async_start_scanner"), + ): + + def update(): + blu_trv_device_mock.return_value.subscribe_updates.call_args[0][0]( + {}, RpcUpdateType.STATUS + ) + + device = _mock_blu_rtv_device() + blu_trv_device_mock.return_value = device + blu_trv_device_mock.return_value.mock_update = Mock(side_effect=update) + + yield blu_trv_device_mock.return_value diff --git a/tests/components/shelly/snapshots/test_binary_sensor.ambr b/tests/components/shelly/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..fcc6377837e --- /dev/null +++ b/tests/components/shelly/snapshots/test_binary_sensor.ambr @@ -0,0 +1,144 @@ +# serializer version: 1 +# name: test_blu_trv_binary_sensor_entity[binary_sensor.trv_name_calibration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.trv_name_calibration', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'TRV-Name calibration', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-blutrv:200-calibration', + 'unit_of_measurement': None, + }) +# --- +# name: test_blu_trv_binary_sensor_entity[binary_sensor.trv_name_calibration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'TRV-Name calibration', + }), + 'context': , + 'entity_id': 'binary_sensor.trv_name_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_rpc_flood_entities[binary_sensor.test_name_flood-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_name_flood', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Test name flood', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-flood:0-flood', + 'unit_of_measurement': None, + }) +# --- +# name: test_rpc_flood_entities[binary_sensor.test_name_flood-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Test name flood', + }), + 'context': , + 'entity_id': 'binary_sensor.test_name_flood', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_rpc_flood_entities[binary_sensor.test_name_mute-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_name_mute', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Test name mute', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-flood:0-mute', + 'unit_of_measurement': None, + }) +# --- +# name: test_rpc_flood_entities[binary_sensor.test_name_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test name mute', + }), + 'context': , + 'entity_id': 'binary_sensor.test_name_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/shelly/snapshots/test_event.ambr b/tests/components/shelly/snapshots/test_event.ambr new file mode 100644 index 00000000000..ae719774aee --- /dev/null +++ b/tests/components/shelly/snapshots/test_event.ambr @@ -0,0 +1,70 @@ +# serializer version: 1 +# name: test_rpc_script_1_event[event.test_name_test_script_js-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'input_event', + 'script_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.test_name_test_script_js', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'test_script.js', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'script', + 'unique_id': '123456789ABC-script:1', + 'unit_of_measurement': None, + }) +# --- +# name: test_rpc_script_1_event[event.test_name_test_script_js-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'input_event', + 'script_start', + ]), + 'friendly_name': 'Test name test_script.js', + }), + 'context': , + 'entity_id': 'event.test_name_test_script_js', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_rpc_script_2_event[event.test_name_test_script_2_js-entry] + None +# --- +# name: test_rpc_script_2_event[event.test_name_test_script_2_js-state] + None +# --- +# name: test_rpc_script_ble_event[event.test_name_aioshelly_ble_integration-entry] + None +# --- +# name: test_rpc_script_ble_event[event.test_name_aioshelly_ble_integration-state] + None +# --- diff --git a/tests/components/shelly/snapshots/test_number.ambr b/tests/components/shelly/snapshots/test_number.ambr new file mode 100644 index 00000000000..07fda999556 --- /dev/null +++ b/tests/components/shelly/snapshots/test_number.ambr @@ -0,0 +1,115 @@ +# serializer version: 1 +# name: test_blu_trv_number_entity[number.trv_name_external_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 50, + 'min': -50, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.trv_name_external_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TRV-Name external temperature', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'external_temperature', + 'unique_id': '123456789ABC-blutrv:200-external_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_blu_trv_number_entity[number.trv_name_external_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'TRV-Name external temperature', + 'max': 50, + 'min': -50, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.trv_name_external_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_blu_trv_number_entity[number.trv_name_valve_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.trv_name_valve_position', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TRV-Name valve position', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'valve_position', + 'unique_id': '123456789ABC-blutrv:200-valve_position', + 'unit_of_measurement': '%', + }) +# --- +# name: test_blu_trv_number_entity[number.trv_name_valve_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'TRV-Name valve position', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.trv_name_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..cb39b148c8a --- /dev/null +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -0,0 +1,156 @@ +# serializer version: 1 +# name: test_blu_trv_sensor_entity[sensor.trv_name_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.trv_name_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'TRV-Name battery', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-blutrv:200-blutrv_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'TRV-Name battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.trv_name_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.trv_name_signal_strength', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'TRV-Name signal strength', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-blutrv:200-blutrv_rssi', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'TRV-Name signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.trv_name_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-60', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.trv_name_valve_position', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'TRV-Name valve position', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'valve_position', + 'unique_id': '123456789ABC-blutrv:200-valve_position', + 'unit_of_measurement': '%', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'TRV-Name valve position', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.trv_name_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index fadfe28db3e..7f2d07b1ccc 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -3,15 +3,15 @@ from copy import deepcopy from unittest.mock import Mock -from aioshelly.const import MODEL_MOTION +from aioshelly.const import MODEL_BLU_GATEWAY_GEN3, MODEL_MOTION from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry @@ -387,7 +387,7 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( ) async def test_rpc_device_virtual_binary_sensor( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, name: str | None, @@ -422,7 +422,7 @@ async def test_rpc_device_virtual_binary_sensor( async def test_rpc_remove_virtual_binary_sensor_when_mode_toggle( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, device_registry: DeviceRegistry, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, @@ -456,7 +456,7 @@ async def test_rpc_remove_virtual_binary_sensor_when_mode_toggle( async def test_rpc_remove_virtual_binary_sensor_when_orphaned( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, device_registry: DeviceRegistry, mock_rpc_device: Mock, ) -> None: @@ -477,3 +477,41 @@ async def test_rpc_remove_virtual_binary_sensor_when_orphaned( entry = entity_registry.async_get(entity_id) assert not entry + + +async def test_blu_trv_binary_sensor_entity( + hass: HomeAssistant, + mock_blu_trv: Mock, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test BLU TRV binary sensor entity.""" + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + for entity in ("calibration",): + entity_id = f"{BINARY_SENSOR_DOMAIN}.trv_name_{entity}" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + +async def test_rpc_flood_entities( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test RPC flood sensor entities.""" + await init_integration(hass, 4) + + for entity in ("flood", "mute"): + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_{entity}" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index aeeeca30edd..5ad298c15a1 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -3,7 +3,12 @@ from copy import deepcopy from unittest.mock import AsyncMock, Mock, PropertyMock -from aioshelly.const import MODEL_VALVE, MODEL_WALL_DISPLAY +from aioshelly.const import ( + BLU_TRV_IDENTIFIER, + MODEL_BLU_GATEWAY_GEN3, + MODEL_VALVE, + MODEL_WALL_DISPLAY, +) from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError import pytest @@ -21,7 +26,7 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.components.shelly.const import DOMAIN +from homeassistant.components.shelly.const import BLU_TRV_TIMEOUT, DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( @@ -37,7 +42,13 @@ from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import MOCK_MAC, init_integration, register_device, register_entity +from . import ( + MOCK_MAC, + get_entity_attribute, + init_integration, + register_device, + register_entity, +) from .conftest import MOCK_STATUS_COAP from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data @@ -294,13 +305,13 @@ async def test_block_restored_climate( assert hass.states.get(entity_id).attributes.get("temperature") == 22.0 -async def test_block_restored_climate_us_customery( +async def test_block_restored_climate_us_customary( hass: HomeAssistant, mock_block_device: Mock, device_registry: DeviceRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test block restored climate with US CUSTOMATY unit system.""" + """Test block restored climate with US CUSTOMARY unit system.""" hass.config.units = US_CUSTOMARY_SYSTEM monkeypatch.delattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "targetTemp") monkeypatch.delattr(mock_block_device.blocks[GAS_VALVE_BLOCK_ID], "targetTemp") @@ -759,3 +770,83 @@ async def test_wall_display_thermostat_mode_external_actuator( entry = entity_registry.async_get(climate_entity_id) assert entry assert entry.unique_id == "123456789ABC-thermostat:0" + + +async def test_blu_trv_climate_set_temperature( + hass: HomeAssistant, + mock_blu_trv: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test BLU TRV set target temperature.""" + + entity_id = "climate.trv_name" + monkeypatch.delitem(mock_blu_trv.status, "thermostat:0") + + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + assert get_entity_attribute(hass, entity_id, ATTR_TEMPERATURE) == 17.1 + + monkeypatch.setitem( + mock_blu_trv.status[f"{BLU_TRV_IDENTIFIER}:200"], "target_C", 28 + ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 28}, + blocking=True, + ) + mock_blu_trv.mock_update() + + mock_blu_trv.call_rpc.assert_called_once_with( + "BluTRV.Call", + { + "id": 200, + "method": "Trv.SetTarget", + "params": {"id": 0, "target_C": 28.0}, + }, + BLU_TRV_TIMEOUT, + ) + + assert get_entity_attribute(hass, entity_id, ATTR_TEMPERATURE) == 28 + + +async def test_blu_trv_climate_disabled( + hass: HomeAssistant, + mock_blu_trv: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test BLU TRV disabled.""" + + entity_id = "climate.trv_name" + monkeypatch.delitem(mock_blu_trv.status, "thermostat:0") + + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + assert get_entity_attribute(hass, entity_id, ATTR_TEMPERATURE) == 17.1 + + monkeypatch.setitem( + mock_blu_trv.config[f"{BLU_TRV_IDENTIFIER}:200"], "enable", False + ) + mock_blu_trv.mock_update() + + assert get_entity_attribute(hass, entity_id, ATTR_TEMPERATURE) is None + + +async def test_blu_trv_climate_hvac_action( + hass: HomeAssistant, + mock_blu_trv: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test BLU TRV is heating.""" + + entity_id = "climate.trv_name" + monkeypatch.delitem(mock_blu_trv.status, "thermostat:0") + + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + assert get_entity_attribute(hass, entity_id, ATTR_HVAC_ACTION) == HVACAction.IDLE + + monkeypatch.setitem(mock_blu_trv.status[f"{BLU_TRV_IDENTIFIER}:200"], "pos", 10) + mock_blu_trv.mock_update() + + assert get_entity_attribute(hass, entity_id, ATTR_HVAC_ACTION) == HVACAction.HEATING diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index d9945706182..b5f87a874c3 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -15,7 +15,6 @@ from aioshelly.exceptions import ( import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.shelly import MacAddressMismatchError, config_flow from homeassistant.components.shelly.const import ( CONF_BLE_SCANNER_MODE, @@ -25,6 +24,10 @@ from homeassistant.components.shelly.const import ( from homeassistant.components.shelly.coordinator import ENTRY_RELOAD_COOLDOWN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -33,22 +36,22 @@ from . import init_integration from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import WebSocketGenerator -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="mock_hostname", name="shelly1pm-12345", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + properties={ATTR_PROPERTIES_ID: "shelly1pm-12345"}, type="mock_type", ) -DISCOVERY_INFO_WITH_MAC = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO_WITH_MAC = ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="mock_hostname", name="shelly1pm-AABBCCDDEEFF", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-AABBCCDDEEFF"}, + properties={ATTR_PROPERTIES_ID: "shelly1pm-AABBCCDDEEFF"}, type="mock_type", ) @@ -1459,13 +1462,13 @@ async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("fd00::b27c:63bb:cc85:4ea0"), ip_addresses=[ip_address("fd00::b27c:63bb:cc85:4ea0")], hostname="mock_hostname", name="shelly1pm-12345", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + properties={ATTR_PROPERTIES_ID: "shelly1pm-12345"}, type="mock_type", ), ) diff --git a/tests/components/shelly/test_event.py b/tests/components/shelly/test_event.py index 2465b016808..e184c154697 100644 --- a/tests/components/shelly/test_event.py +++ b/tests/components/shelly/test_event.py @@ -2,9 +2,11 @@ from unittest.mock import Mock +from aioshelly.ble.const import BLE_SCRIPT_NAME from aioshelly.const import MODEL_I3 import pytest from pytest_unordered import unordered +from syrupy import SnapshotAssertion from homeassistant.components.event import ( ATTR_EVENT_TYPE, @@ -64,6 +66,99 @@ async def test_rpc_button( assert state.attributes.get(ATTR_EVENT_TYPE) == "single_push" +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_rpc_script_1_event( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, + snapshot: SnapshotAssertion, +) -> None: + """Test script event.""" + await init_integration(hass, 2) + entity_id = "event.test_name_test_script_js" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "script:1", + "id": 1, + "event": "script_start", + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_EVENT_TYPE) == "script_start" + + inject_rpc_device_event( + monkeypatch, + mock_rpc_device, + { + "events": [ + { + "component": "script:1", + "id": 1, + "event": "unknown_event", + "ts": 1668522399.2, + } + ], + "ts": 1668522399.2, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_EVENT_TYPE) != "unknown_event" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_rpc_script_2_event( + hass: HomeAssistant, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test that scripts without any emitEvent will not get an event entity.""" + await init_integration(hass, 2) + entity_id = "event.test_name_test_script_2_js" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_rpc_script_ble_event( + hass: HomeAssistant, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test that the ble script will not get an event entity.""" + await init_integration(hass, 2) + entity_id = f"event.test_name_{BLE_SCRIPT_NAME}" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + async def test_rpc_event_removal( hass: HomeAssistant, mock_rpc_device: Mock, diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index b5516485501..270e2163635 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -545,3 +545,22 @@ async def test_sleeping_block_device_wrong_sleep_period( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.data[CONF_SLEEP_PERIOD] == BLOCK_EXPECTED_SLEEP_PERIOD + + +async def test_bluetooth_cleanup_on_remove_entry( + hass: HomeAssistant, + mock_rpc_device: Mock, +) -> None: + """Test bluetooth is cleaned up on entry removal.""" + entry = await init_integration(hass, 2) + + assert entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + with patch("homeassistant.components.shelly.async_remove_scanner") as remove_mock: + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + remove_mock.assert_called_once_with(hass, entry.unique_id.upper()) diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 6c1cc394b64..b1b65d99ab5 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -3,8 +3,10 @@ from copy import deepcopy from unittest.mock import AsyncMock, Mock +from aioshelly.const import MODEL_BLU_GATEWAY_GEN3 from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError import pytest +from syrupy import SnapshotAssertion from homeassistant.components.number import ( ATTR_MAX, @@ -16,7 +18,7 @@ from homeassistant.components.number import ( SERVICE_SET_VALUE, NumberMode, ) -from homeassistant.components.shelly.const import DOMAIN +from homeassistant.components.shelly.const import BLU_TRV_TIMEOUT, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State @@ -390,3 +392,102 @@ async def test_rpc_remove_virtual_number_when_orphaned( entry = entity_registry.async_get(entity_id) assert not entry + + +async def test_blu_trv_number_entity( + hass: HomeAssistant, + mock_blu_trv: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, + snapshot: SnapshotAssertion, +) -> None: + """Test BLU TRV number entity.""" + # disable automatic temperature control in the device + monkeypatch.setitem(mock_blu_trv.config["blutrv:200"], "enable", False) + + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + for entity in ("external_temperature", "valve_position"): + entity_id = f"{NUMBER_DOMAIN}.trv_name_{entity}" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + +async def test_blu_trv_ext_temp_set_value( + hass: HomeAssistant, mock_blu_trv: Mock +) -> None: + """Test the set value action for BLU TRV External Temperature number entity.""" + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + entity_id = f"{NUMBER_DOMAIN}.trv_name_external_temperature" + + # After HA start the state should be unknown because there was no previous external + # temperature report + assert hass.states.get(entity_id).state is STATE_UNKNOWN + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 22.2, + }, + blocking=True, + ) + mock_blu_trv.mock_update() + mock_blu_trv.call_rpc.assert_called_once_with( + "BluTRV.Call", + { + "id": 200, + "method": "Trv.SetExternalTemperature", + "params": {"id": 0, "t_C": 22.2}, + }, + BLU_TRV_TIMEOUT, + ) + + assert hass.states.get(entity_id).state == "22.2" + + +async def test_blu_trv_valve_pos_set_value( + hass: HomeAssistant, + mock_blu_trv: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test the set value action for BLU TRV Valve Position number entity.""" + # disable automatic temperature control to enable valve position entity + monkeypatch.setitem(mock_blu_trv.config["blutrv:200"], "enable", False) + + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + entity_id = f"{NUMBER_DOMAIN}.trv_name_valve_position" + + assert hass.states.get(entity_id).state == "0" + + monkeypatch.setitem(mock_blu_trv.status["blutrv:200"], "pos", 20) + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 20.0, + }, + blocking=True, + ) + mock_blu_trv.mock_update() + mock_blu_trv.call_rpc.assert_called_once_with( + "BluTRV.Call", + { + "id": 200, + "method": "Trv.SetPosition", + "params": {"id": 0, "pos": 20}, + }, + BLU_TRV_TIMEOUT, + ) + # device only accepts int for 'pos' value + assert isinstance(mock_blu_trv.call_rpc.call_args[0][1]["params"]["pos"], int) + + assert hass.states.get(entity_id).state == "20" diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 18c3d874c55..ef7771e53ba 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -3,8 +3,10 @@ from copy import deepcopy from unittest.mock import Mock +from aioshelly.const import MODEL_BLU_GATEWAY_GEN3 from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy import SnapshotAssertion from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -428,14 +430,16 @@ async def test_rpc_sensor_error( assert hass.states.get(entity_id).state == "4.321" - mutate_rpc_device_status(monkeypatch, mock_rpc_device, "voltmeter", "voltage", None) + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "voltmeter:100", "voltage", None + ) mock_rpc_device.mock_update() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE entry = entity_registry.async_get(entity_id) assert entry - assert entry.unique_id == "123456789ABC-voltmeter-voltmeter" + assert entry.unique_id == "123456789ABC-voltmeter:100-voltmeter" async def test_rpc_polling_sensor( @@ -1383,3 +1387,71 @@ async def test_rpc_device_sensor_goes_unavailable_on_disconnect( await hass.async_block_till_done() temp_sensor_state = hass.states.get("sensor.test_name_temperature") assert temp_sensor_state.state != STATE_UNAVAILABLE + + +async def test_rpc_voltmeter_value( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, +) -> None: + """Test RPC voltmeter value sensor.""" + entity_id = f"{SENSOR_DOMAIN}.test_name_voltmeter_value" + + await init_integration(hass, 2) + + state = hass.states.get(entity_id) + + assert state.state == "12.34" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "ppm" + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-voltmeter:100-voltmeter_value" + + +async def test_blu_trv_sensor_entity( + hass: HomeAssistant, + mock_blu_trv: Mock, + entity_registry: EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test BLU TRV sensor entity.""" + await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3) + + for entity in ("battery", "signal_strength", "valve_position"): + entity_id = f"{SENSOR_DOMAIN}.trv_name_{entity}" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + +async def test_rpc_device_virtual_number_sensor_with_device_class( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test a virtual number sensor with device class for RPC device.""" + config = deepcopy(mock_rpc_device.config) + config["number:203"] = { + "name": "Current humidity", + "min": 0, + "max": 100, + "meta": {"ui": {"step": 1, "unit": "%", "view": "label"}}, + "role": "current_humidity", + } + monkeypatch.setattr(mock_rpc_device, "config", config) + + status = deepcopy(mock_rpc_device.status) + status["number:203"] = {"value": 34} + monkeypatch.setattr(mock_rpc_device, "status", status) + + await init_integration(hass, 3) + + state = hass.states.get("sensor.test_name_current_humidity") + assert state + assert state.state == "34" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 5c7933afd7e..5aae9dfffc9 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry @@ -444,7 +443,7 @@ async def test_wall_display_relay_mode( ) async def test_rpc_device_virtual_switch( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, name: str | None, @@ -517,7 +516,7 @@ async def test_rpc_device_virtual_binary_sensor( async def test_rpc_remove_virtual_switch_when_mode_label( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, device_registry: DeviceRegistry, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, @@ -551,7 +550,7 @@ async def test_rpc_remove_virtual_switch_when_mode_label( async def test_rpc_remove_virtual_switch_when_orphaned( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, device_registry: DeviceRegistry, mock_rpc_device: Mock, ) -> None: @@ -577,7 +576,7 @@ async def test_rpc_remove_virtual_switch_when_orphaned( @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_rpc_device_script_switch( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index cd4cdf877a5..9ea66c1acb7 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -9,6 +9,7 @@ import pytest from homeassistant.components.shelly.const import ( DOMAIN, GEN1_RELEASE_URL, + GEN2_BETA_RELEASE_URL, GEN2_RELEASE_URL, ) from homeassistant.components.update import ( @@ -572,7 +573,6 @@ async def test_rpc_beta_update( assert state.attributes[ATTR_LATEST_VERSION] == "1" assert state.attributes[ATTR_IN_PROGRESS] is False assert state.attributes[ATTR_UPDATE_PERCENTAGE] is None - assert state.attributes[ATTR_RELEASE_URL] is None monkeypatch.setitem( mock_rpc_device.status["sys"], @@ -589,7 +589,7 @@ async def test_rpc_beta_update( assert state.attributes[ATTR_INSTALLED_VERSION] == "1" assert state.attributes[ATTR_LATEST_VERSION] == "2b" assert state.attributes[ATTR_IN_PROGRESS] is False - assert state.attributes[ATTR_UPDATE_PERCENTAGE] is None + assert state.attributes[ATTR_RELEASE_URL] == GEN2_BETA_RELEASE_URL await hass.services.async_call( UPDATE_DOMAIN, diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index 17bcd6e3d40..b7c3dff10f6 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -17,7 +17,11 @@ from aioshelly.const import ( ) import pytest -from homeassistant.components.shelly.const import GEN1_RELEASE_URL, GEN2_RELEASE_URL +from homeassistant.components.shelly.const import ( + GEN1_RELEASE_URL, + GEN2_BETA_RELEASE_URL, + GEN2_RELEASE_URL, +) from homeassistant.components.shelly.utils import ( get_block_channel_name, get_block_device_sleep_period, @@ -300,7 +304,7 @@ async def test_get_rpc_input_triggers( (1, MODEL_1, True, None), (2, MODEL_WALL_DISPLAY, False, None), (2, MODEL_PLUS_2PM_V2, False, GEN2_RELEASE_URL), - (2, MODEL_PLUS_2PM_V2, True, None), + (2, MODEL_PLUS_2PM_V2, True, GEN2_BETA_RELEASE_URL), ], ) def test_get_release_url( diff --git a/tests/components/shelly/test_valve.py b/tests/components/shelly/test_valve.py index b35ce98b664..9dc8597120a 100644 --- a/tests/components/shelly/test_valve.py +++ b/tests/components/shelly/test_valve.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN, ValveState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry from . import init_integration @@ -17,7 +17,7 @@ GAS_VALVE_BLOCK_ID = 6 async def test_block_device_gas_valve( hass: HomeAssistant, - entity_registry: er.EntityRegistry, + entity_registry: EntityRegistry, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 5db6347a832..ba03f6fc804 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -11,7 +11,7 @@ import pytest import simplehound.core as hound from homeassistant.components.image_processing import DOMAIN as IP_DOMAIN, SERVICE_SCAN -import homeassistant.components.sighthound.image_processing as sh +from homeassistant.components.sighthound import image_processing as sh from homeassistant.const import ( ATTR_ENTITY_ID, CONF_API_KEY, diff --git a/tests/components/simplefin/snapshots/test_binary_sensor.ambr b/tests/components/simplefin/snapshots/test_binary_sensor.ambr index 44fe2a10b78..3123100205e 100644 --- a/tests/components/simplefin/snapshots/test_binary_sensor.ambr +++ b/tests/components/simplefin/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -246,6 +251,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +300,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -342,6 +349,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/simplefin/snapshots/test_sensor.ambr b/tests/components/simplefin/snapshots/test_sensor.ambr index c7dced9300e..dd305f7528f 100644 --- a/tests/components/simplefin/snapshots/test_sensor.ambr +++ b/tests/components/simplefin/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -109,6 +111,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -160,6 +163,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -210,6 +214,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -261,6 +266,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -311,6 +317,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -362,6 +369,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -412,6 +420,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -463,6 +472,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -513,6 +523,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -564,6 +575,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -614,6 +626,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -665,6 +678,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -715,6 +729,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -766,6 +781,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/slack/__init__.py b/tests/components/slack/__init__.py index acb52a11a6c..507e96294ff 100644 --- a/tests/components/slack/__init__.py +++ b/tests/components/slack/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -AUTH_URL = "https://www.slack.com/api/auth.test" +AUTH_URL = "https://slack.com/api/auth.test" TOKEN = "abc123" TEAM_NAME = "Test Team" diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index 565b5ec2149..6d0953da5e9 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -81,7 +81,7 @@ async def test_flow_user_cannot_connect( async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: """Test user initialized flow with unreachable server.""" with patch( - "homeassistant.components.slack.config_flow.WebClient.auth_test" + "homeassistant.components.slack.config_flow.AsyncWebClient.auth_test" ) as mock: mock.side_effect = Exception result = await hass.config_entries.flow.async_init( diff --git a/tests/components/slide_local/snapshots/test_button.ambr b/tests/components/slide_local/snapshots/test_button.ambr index 549538f1361..7b363f4d9ba 100644 --- a/tests/components/slide_local/snapshots/test_button.ambr +++ b/tests/components/slide_local/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/slide_local/snapshots/test_cover.ambr b/tests/components/slide_local/snapshots/test_cover.ambr new file mode 100644 index 00000000000..172f5411a94 --- /dev/null +++ b/tests/components/slide_local/snapshots/test_cover.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_all_entities[cover.slide_bedroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.slide_bedroom', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'slide_local', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '1234567890ab', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[cover.slide_bedroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'current_position': 100, + 'device_class': 'curtain', + 'friendly_name': 'slide bedroom', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.slide_bedroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/slide_local/snapshots/test_diagnostics.ambr b/tests/components/slide_local/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..7606c2a399b --- /dev/null +++ b/tests/components/slide_local/snapshots/test_diagnostics.ambr @@ -0,0 +1,41 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'config_entry': dict({ + 'data': dict({ + 'api_version': 2, + 'host': '127.0.0.2', + 'mac': '12:34:56:78:90:ab', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'slide_local', + 'entry_id': 'ce5f5431554d101905d31797e1232da8', + 'minor_version': 1, + 'options': dict({ + 'invert_position': False, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'slide', + 'unique_id': '12:34:56:78:90:ab', + 'version': 1, + }), + 'slide_data': dict({ + 'board_rev': 1, + 'calib_time': 10239, + 'curtain_type': 0, + 'device_name': 'slide bedroom', + 'mac': '1234567890ab', + 'pos': 0, + 'slide_id': 'slide_1234567890ab', + 'state': 'open', + 'touch_go': True, + 'zone_name': 'bedroom', + }), + }) +# --- diff --git a/tests/components/slide_local/snapshots/test_init.ambr b/tests/components/slide_local/snapshots/test_init.ambr index d90f72e4b05..5b1a9f5ce2f 100644 --- a/tests/components/slide_local/snapshots/test_init.ambr +++ b/tests/components/slide_local/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.2', 'connections': set({ tuple( diff --git a/tests/components/slide_local/snapshots/test_switch.ambr b/tests/components/slide_local/snapshots/test_switch.ambr index e19467c283e..9b1a7969539 100644 --- a/tests/components/slide_local/snapshots/test_switch.ambr +++ b/tests/components/slide_local/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/slide_local/test_button.py b/tests/components/slide_local/test_button.py index 646c8fd7ef3..c232affbb99 100644 --- a/tests/components/slide_local/test_button.py +++ b/tests/components/slide_local/test_button.py @@ -2,11 +2,19 @@ from unittest.mock import AsyncMock +from goslideapi.goslideapi import ( + AuthenticationFailed, + ClientConnectionError, + ClientTimeoutError, + DigestAuthCalcError, +) +import pytest from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_platform @@ -44,3 +52,37 @@ async def test_pressing_button( blocking=True, ) mock_slide_api.slide_calibrate.assert_called_once() + + +@pytest.mark.parametrize( + ("exception"), + [ + ClientConnectionError, + ClientTimeoutError, + AuthenticationFailed, + DigestAuthCalcError, + ], +) +async def test_pressing_button_exception( + hass: HomeAssistant, + exception: Exception, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing button.""" + await setup_platform(hass, mock_config_entry, [Platform.BUTTON]) + + mock_slide_api.slide_calibrate.side_effect = exception + + with pytest.raises( + HomeAssistantError, + match="Error while sending the calibration request to the device", + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.slide_bedroom_calibrate", + }, + blocking=True, + ) diff --git a/tests/components/slide_local/test_config_flow.py b/tests/components/slide_local/test_config_flow.py index 48be7dd7850..b8b69d99fd8 100644 --- a/tests/components/slide_local/test_config_flow.py +++ b/tests/components/slide_local/test_config_flow.py @@ -12,11 +12,11 @@ from goslideapi.goslideapi import ( import pytest from homeassistant.components.slide_local.const import CONF_INVERT_POSITION, DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from . import setup_platform from .const import HOST, SLIDE_INFO_DATA @@ -282,6 +282,36 @@ async def test_abort_if_already_setup( assert result["reason"] == "already_configured" +async def test_reconfigure( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test reconfigure flow options.""" + + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "127.0.0.3", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry + assert entry.data[CONF_HOST] == "127.0.0.3" + + async def test_zeroconf( hass: HomeAssistant, mock_slide_api: AsyncMock, mock_setup_entry: AsyncMock ) -> None: diff --git a/tests/components/slide_local/test_cover.py b/tests/components/slide_local/test_cover.py new file mode 100644 index 00000000000..e0e4a0741d8 --- /dev/null +++ b/tests/components/slide_local/test_cover.py @@ -0,0 +1,215 @@ +"""Tests for the Slide Local cover platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +from goslideapi.goslideapi import ClientConnectionError +from syrupy import SnapshotAssertion + +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + CoverState, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_platform +from .const import SLIDE_INFO_DATA + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_connection_error( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test connection error.""" + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + mock_slide_api.slide_info.side_effect = [ClientConnectionError, SLIDE_INFO_DATA] + + freezer.tick(delta=timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == STATE_UNAVAILABLE + + freezer.tick(delta=timedelta(minutes=2)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.OPEN + + +async def test_state_change( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test connection error.""" + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + mock_slide_api.slide_info.side_effect = [ + dict(SLIDE_INFO_DATA, pos=0.0), + dict(SLIDE_INFO_DATA, pos=0.4), + dict(SLIDE_INFO_DATA, pos=1.0), + dict(SLIDE_INFO_DATA, pos=0.8), + ] + + freezer.tick(delta=timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.OPEN + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.CLOSING + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.CLOSED + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.OPENING + + +async def test_open_cover( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test open cover.""" + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + { + ATTR_ENTITY_ID: "cover.slide_bedroom", + }, + blocking=True, + ) + mock_slide_api.slide_open.assert_called_once() + + +async def test_close_cover( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test close cover.""" + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + { + ATTR_ENTITY_ID: "cover.slide_bedroom", + }, + blocking=True, + ) + mock_slide_api.slide_close.assert_called_once() + + +async def test_stop_cover( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test stop cover.""" + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + { + ATTR_ENTITY_ID: "cover.slide_bedroom", + }, + blocking=True, + ) + mock_slide_api.slide_stop.assert_called_once() + + +async def test_set_position( + hass: HomeAssistant, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test set cover position.""" + + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + + mock_slide_api.slide_info.side_effect = [ + dict(SLIDE_INFO_DATA, pos=0.0), + dict(SLIDE_INFO_DATA, pos=1.0), + dict(SLIDE_INFO_DATA, pos=1.0), + dict(SLIDE_INFO_DATA, pos=0.0), + ] + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.slide_bedroom", ATTR_POSITION: 1.0}, + blocking=True, + ) + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.CLOSED + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.slide_bedroom", ATTR_POSITION: 0.0}, + blocking=True, + ) + + freezer.tick(delta=timedelta(seconds=15)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("cover.slide_bedroom").state == CoverState.OPEN + + assert len(mock_slide_api.slide_set_position.mock_calls) == 2 diff --git a/tests/components/slide_local/test_diagnostics.py b/tests/components/slide_local/test_diagnostics.py new file mode 100644 index 00000000000..3e11af378c5 --- /dev/null +++ b/tests/components/slide_local/test_diagnostics.py @@ -0,0 +1,34 @@ +"""Test slide_local diagnostics.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import setup_platform + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_config_entry: MockConfigEntry, + mock_slide_api: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + await setup_platform( + hass, mock_config_entry, [Platform.BUTTON, Platform.COVER, Platform.SWITCH] + ) + + result = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + + assert result == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/slide_local/test_init.py b/tests/components/slide_local/test_init.py index 7b0a2d83164..ec9a12f9eeb 100644 --- a/tests/components/slide_local/test_init.py +++ b/tests/components/slide_local/test_init.py @@ -2,8 +2,10 @@ from unittest.mock import AsyncMock +from goslideapi.goslideapi import ClientConnectionError from syrupy import SnapshotAssertion +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -27,3 +29,37 @@ async def test_device_info( ) assert device_entry is not None assert device_entry == snapshot + + +async def test_raise_config_entry_not_ready_when_offline( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_slide_api: AsyncMock, +) -> None: + """Config entry state is SETUP_RETRY when slide is offline.""" + + mock_slide_api.slide_info.side_effect = [ClientConnectionError, None] + + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_raise_config_entry_not_ready_when_empty_data( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_slide_api: AsyncMock, +) -> None: + """Config entry state is SETUP_RETRY when slide is offline.""" + + mock_slide_api.slide_info.return_value = None + + await setup_platform(hass, mock_config_entry, [Platform.COVER]) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + assert len(hass.config_entries.flow.async_progress()) == 0 diff --git a/tests/components/slide_local/test_switch.py b/tests/components/slide_local/test_switch.py index 0ac9820ca10..9d0d8274aa5 100644 --- a/tests/components/slide_local/test_switch.py +++ b/tests/components/slide_local/test_switch.py @@ -2,6 +2,12 @@ from unittest.mock import AsyncMock +from goslideapi.goslideapi import ( + AuthenticationFailed, + ClientConnectionError, + ClientTimeoutError, + DigestAuthCalcError, +) import pytest from syrupy import SnapshotAssertion @@ -13,6 +19,7 @@ from homeassistant.components.switch import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_platform @@ -59,3 +66,38 @@ async def test_services( blocking=True, ) mock_slide_api.slide_set_touchgo.assert_called_once() + + +@pytest.mark.parametrize( + ("exception", "service"), + [ + (ClientConnectionError, SERVICE_TURN_OFF), + (ClientTimeoutError, SERVICE_TURN_ON), + (AuthenticationFailed, SERVICE_TURN_OFF), + (DigestAuthCalcError, SERVICE_TURN_ON), + ], +) +async def test_service_exception( + hass: HomeAssistant, + exception: Exception, + service: str, + mock_slide_api: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing button.""" + await setup_platform(hass, mock_config_entry, [Platform.SWITCH]) + + mock_slide_api.slide_set_touchgo.side_effect = exception + + with pytest.raises( + HomeAssistantError, + match=f"Error while sending the request setting Touch&Go to {service[5:]} to the device", + ): + await hass.services.async_call( + SWITCH_DOMAIN, + service, + { + ATTR_ENTITY_ID: "switch.slide_bedroom_touchgo", + }, + blocking=True, + ) diff --git a/tests/components/sma/snapshots/test_diagnostics.ambr b/tests/components/sma/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..14b0d120190 --- /dev/null +++ b/tests/components/sma/snapshots/test_diagnostics.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_get_config_entry_diagnostics + dict({ + 'entities': dict({ + }), + 'entry': dict({ + 'data': dict({ + 'group': 'user', + 'host': '1.1.1.1', + 'password': '**REDACTED**', + 'ssl': True, + 'verify_ssl': False, + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'sma', + 'minor_version': 2, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'import', + 'subentries': list([ + ]), + 'title': 'SMA Device Name', + 'unique_id': '123456789', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/sma/test_diagnostics.py b/tests/components/sma/test_diagnostics.py new file mode 100644 index 00000000000..6c1fe0dc5cb --- /dev/null +++ b/tests/components/sma/test_diagnostics.py @@ -0,0 +1,29 @@ +"""Test the SMA diagnostics.""" + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_get_config_entry_diagnostics( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + hass_client: ClientSessionGenerator, + mock_config_entry: MockConfigEntry, +) -> None: + """Test if get_config_entry_diagnostics returns the correct data.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + assert diagnostics == snapshot( + exclude=props("created_at", "modified_at", "entry_id") + ) diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index c06ab551ef6..205c700a402 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest from homeassistant import setup -from homeassistant.components import zeroconf from homeassistant.components.smappee.const import ( CONF_SERIALNUMBER, DOMAIN, @@ -20,6 +19,7 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -63,7 +63,7 @@ async def test_show_zeroconf_connection_error_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -95,7 +95,7 @@ async def test_show_zeroconf_connection_error_form_next_generation( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -179,7 +179,7 @@ async def test_zeroconf_wrong_mdns(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -305,7 +305,7 @@ async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -355,7 +355,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -377,7 +377,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -504,7 +504,7 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, @@ -589,7 +589,7 @@ async def test_full_zeroconf_flow_next_generation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], port=22, diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 3621e58bc3d..05ddc3a71de 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.smartthings.const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, + CONF_REFRESH_TOKEN, DOMAIN, ) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET @@ -757,3 +758,56 @@ async def test_no_available_locations_aborts( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_available_locations" + + +async def test_reauth( + hass: HomeAssistant, app, app_oauth_client, location, smartthings_mock +) -> None: + """Test reauth flow.""" + token = str(uuid4()) + installed_app_id = str(uuid4()) + refresh_token = str(uuid4()) + smartthings_mock.apps.return_value = [] + smartthings_mock.create_app.return_value = (app, app_oauth_client) + smartthings_mock.locations.return_value = [location] + request = Mock() + request.installed_app_id = installed_app_id + request.auth_token = token + request.location_id = location.location_id + request.refresh_token = refresh_token + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_APP_ID: app.app_id, + CONF_CLIENT_ID: app_oauth_client.client_id, + CONF_CLIENT_SECRET: app_oauth_client.client_secret, + CONF_LOCATION_ID: location.location_id, + CONF_INSTALLED_APP_ID: installed_app_id, + CONF_ACCESS_TOKEN: token, + CONF_REFRESH_TOKEN: "abc", + }, + unique_id=smartapp.format_unique_id(app.app_id, location.location_id), + ) + entry.add_to_hass(hass) + result = await entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.EXTERNAL_STEP + assert result["step_id"] == "authorize" + assert result["url"] == format_install_url(app.app_id, location.location_id) + + await smartapp.smartapp_update(hass, request, None, app) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "update_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert entry.data[CONF_REFRESH_TOKEN] == refresh_token diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index e518f84aecb..83372b58228 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -23,6 +23,7 @@ from homeassistant.components.smartthings.const import ( PLATFORMS, SIGNAL_SMARTTHINGS_UPDATE, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.core_config import async_process_ha_core_config from homeassistant.exceptions import ConfigEntryNotReady @@ -68,17 +69,10 @@ async def test_unrecoverable_api_errors_create_new_flow( ) # Assert setup returns false - result = await smartthings.async_setup_entry(hass, config_entry) + result = await hass.config_entries.async_setup(config_entry.entry_id) assert not result - # Assert entry was removed and new flow created - await hass.async_block_till_done() - assert not hass.config_entries.async_entries(DOMAIN) - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["handler"] == "smartthings" - assert flows[0]["context"] == {"source": config_entries.SOURCE_IMPORT} - hass.config_entries.flow.async_abort(flows[0]["flow_id"]) + assert config_entry.state == ConfigEntryState.SETUP_ERROR async def test_recoverable_api_errors_raise_not_ready( diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 021ee9cc810..a6a48202f1d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -4,14 +4,9 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ -from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability +from pysmartthings import Attribute, Capability -from homeassistant.components.sensor import ( - DEVICE_CLASSES, - DOMAIN as SENSOR_DOMAIN, - STATE_CLASSES, -) -from homeassistant.components.smartthings import sensor +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -29,20 +24,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform -async def test_mapping_integrity() -> None: - """Test ensures the map dicts have proper integrity.""" - for capability, maps in sensor.CAPABILITY_TO_SENSORS.items(): - assert capability in CAPABILITIES, capability - for sensor_map in maps: - assert sensor_map.attribute in ATTRIBUTES, sensor_map.attribute - if sensor_map.device_class: - assert ( - sensor_map.device_class in DEVICE_CLASSES - ), sensor_map.device_class - if sensor_map.state_class: - assert sensor_map.state_class in STATE_CLASSES, sensor_map.state_class - - async def test_entity_state(hass: HomeAssistant, device_factory) -> None: """Tests the state attributes properly match the sensor types.""" device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) @@ -75,7 +56,9 @@ async def test_entity_three_axis_invalid_state( ) -> None: """Tests the state attributes properly match the three axis types.""" device = device_factory( - "Three Axis", [Capability.three_axis], {Attribute.three_axis: []} + "Three Axis", + [Capability.three_axis], + {Attribute.three_axis: [None, None, None]}, ) await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) state = hass.states.get("sensor.three_axis_x_coordinate") diff --git a/tests/components/smarty/snapshots/test_binary_sensor.ambr b/tests/components/smarty/snapshots/test_binary_sensor.ambr index 2f943a25012..ad4b61f5070 100644 --- a/tests/components/smarty/snapshots/test_binary_sensor.ambr +++ b/tests/components/smarty/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smarty/snapshots/test_button.ambr b/tests/components/smarty/snapshots/test_button.ambr index 38849bd2b2e..b5b86c80beb 100644 --- a/tests/components/smarty/snapshots/test_button.ambr +++ b/tests/components/smarty/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smarty/snapshots/test_fan.ambr b/tests/components/smarty/snapshots/test_fan.ambr index 8ca95beeb86..2502bd6f09f 100644 --- a/tests/components/smarty/snapshots/test_fan.ambr +++ b/tests/components/smarty/snapshots/test_fan.ambr @@ -8,6 +8,7 @@ 'preset_modes': None, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smarty/snapshots/test_init.ambr b/tests/components/smarty/snapshots/test_init.ambr index b25cdb9dc3a..a292cc97f47 100644 --- a/tests/components/smarty/snapshots/test_init.ambr +++ b/tests/components/smarty/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/smarty/snapshots/test_sensor.ambr b/tests/components/smarty/snapshots/test_sensor.ambr index 2f713db7f83..c32740fa38c 100644 --- a/tests/components/smarty/snapshots/test_sensor.ambr +++ b/tests/components/smarty/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -101,6 +103,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -148,6 +151,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -196,6 +200,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -244,6 +249,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smarty/snapshots/test_switch.ambr b/tests/components/smarty/snapshots/test_switch.ambr index be1da7c6961..33c829adf31 100644 --- a/tests/components/smarty/snapshots/test_switch.ambr +++ b/tests/components/smarty/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index a0bbf854699..0e65d288737 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -2,7 +2,6 @@ ENTITY_ID = "weather.smhi_test" TEST_CONFIG = { - "name": "test", "location": { "longitude": "17.84197", "latitude": "59.32624", @@ -11,5 +10,5 @@ TEST_CONFIG = { TEST_CONFIG_MIGRATE = { "name": "test", "longitude": "17.84197", - "latitude": "17.84197", + "latitude": "59.32624", } diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 4195d1e5d52..524aad873f9 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from smhi.smhi_lib import SmhiForecastException +from pysmhi import SmhiForecastException from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN @@ -31,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -57,7 +57,6 @@ async def test_form(hass: HomeAssistant) -> None: "latitude": 0.0, "longitude": 0.0, }, - "name": "Home", } assert len(mock_setup_entry.mock_calls) == 1 @@ -67,7 +66,7 @@ async def test_form(hass: HomeAssistant) -> None: ) with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -93,7 +92,6 @@ async def test_form(hass: HomeAssistant) -> None: "latitude": 1.0, "longitude": 1.0, }, - "name": "Weather", } @@ -104,7 +102,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException, ): result2 = await hass.config_entries.flow.async_configure( @@ -124,7 +122,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: # Continue flow with new coordinates with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -150,7 +148,6 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: "latitude": 2.0, "longitude": 2.0, }, - "name": "Weather", } @@ -173,7 +170,7 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ): result2 = await hass.config_entries.flow.async_configure( @@ -201,8 +198,8 @@ async def test_reconfigure_flow( domain=DOMAIN, title="Home", unique_id="57.2898-13.6304", - data={"location": {"latitude": 57.2898, "longitude": 13.6304}, "name": "Home"}, - version=2, + data={"location": {"latitude": 57.2898, "longitude": 13.6304}}, + version=3, ) entry.add_to_hass(hass) @@ -221,7 +218,7 @@ async def test_reconfigure_flow( assert result["type"] is FlowResultType.FORM with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException, ): result = await hass.config_entries.flow.async_configure( @@ -240,7 +237,7 @@ async def test_reconfigure_flow( with ( patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + "homeassistant.components.smhi.config_flow.SMHIPointForecast.async_get_daily_forecast", return_value={"test": "something", "test2": "something else"}, ), patch( @@ -269,7 +266,6 @@ async def test_reconfigure_flow( "latitude": 58.2898, "longitude": 14.6304, }, - "name": "Home", } entity = entity_registry.async_get(entity.entity_id) assert entity diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index cfb386c8f6f..f301e684e3e 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,10 +1,9 @@ """Test SMHI component setup process.""" -from unittest.mock import patch - -from smhi.smhi_lib import APIURL_TEMPLATE +from pysmhi.const import API_POINT_FORECAST from homeassistant.components.smhi.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -18,11 +17,11 @@ async def test_setup_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test setup entry.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain=DOMAIN, title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -36,11 +35,11 @@ async def test_remove_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test remove entry.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain=DOMAIN, title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -63,7 +62,7 @@ async def test_migrate_entry( api_response: str, ) -> None: """Test migrate entry data.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) aioclient_mock.get(uri, text=api_response) @@ -77,7 +76,7 @@ async def test_migrate_entry( original_name="Weather", platform="smhi", supported_features=0, - unique_id="17.84197, 17.84197", + unique_id="59.32624, 17.84197", ) await hass.config_entries.async_setup(entry.entry_id) @@ -86,30 +85,27 @@ async def test_migrate_entry( state = hass.states.get(entity.entity_id) assert state - assert entry.version == 2 - assert entry.unique_id == "17.84197-17.84197" + assert entry.version == 3 + assert entry.unique_id == "59.32624-17.84197" + assert entry.data == TEST_CONFIG entity_get = entity_registry.async_get(entity.entity_id) - assert entity_get.unique_id == "17.84197, 17.84197" + assert entity_get.unique_id == "59.32624, 17.84197" -async def test_migrate_entry_failed( +async def test_migrate_from_future_version( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: - """Test migrate entry data that fails.""" - uri = APIURL_TEMPLATE.format( + """Test migrate entry not possible from future version.""" + uri = API_POINT_FORECAST.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG_MIGRATE) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG_MIGRATE, version=4) entry.add_to_hass(hass) - assert entry.version == 1 + assert entry.version == 4 - with patch( - "homeassistant.config_entries.ConfigEntries.async_update_entry", - return_value=False, - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - assert entry.version == 1 + assert entry.state == ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 1870d7b498a..a39cb72d4b8 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -4,8 +4,9 @@ from datetime import datetime, timedelta from unittest.mock import patch from freezegun import freeze_time +from pysmhi import SMHIForecast, SmhiForecastException +from pysmhi.const import API_POINT_FORECAST import pytest -from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException from syrupy.assertion import SnapshotAssertion from homeassistant.components.smhi.const import ATTR_SMHI_THUNDER_PROBABILITY @@ -44,17 +45,17 @@ async def test_setup_hass( snapshot: SnapshotAssertion, ) -> None: """Test for successfully setting up the smhi integration.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 # Testing the actual entity state for # deeper testing than normal unity test @@ -75,17 +76,17 @@ async def test_clear_night( """Test for successfully setting up the smhi integration.""" hass.config.latitude = "59.32624" hass.config.longitude = "17.84197" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response_night) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 state = hass.states.get(ENTITY_ID) @@ -105,11 +106,11 @@ async def test_clear_night( async def test_properties_no_data(hass: HomeAssistant) -> None: """Test properties when no API data available.""" - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", side_effect=SmhiForecastException("boom"), ): await hass.config_entries.async_setup(entry.entry_id) @@ -134,75 +135,91 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: """Test behaviour when unknown symbol from API.""" - data = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 1, 0, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 1, 0, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) - - data2 = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data2 = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 1, 12, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 1, 12, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) - - data3 = SmhiForecast( - temperature=5, - temperature_max=10, - temperature_min=0, - humidity=5, - pressure=1008, - thunder=0, - cloudiness=52, - precipitation=1, - wind_direction=180, - wind_speed=10, - horizontal_visibility=6, - wind_gust=1.5, - mean_precipitation=0.5, - total_precipitation=1, + data3 = SMHIForecast( + frozen_precipitation=0, + high_cloud=100, + humidity=96, + low_cloud=100, + max_precipitation=0.0, + mean_precipitation=0.0, + median_precipitation=0.0, + medium_cloud=75, + min_precipitation=0.0, + precipitation_category=0, + pressure=1018.9, symbol=100, # Faulty symbol - valid_time=datetime(2018, 1, 2, 12, 1, 2), + temperature=1.0, + temperature_max=1.0, + temperature_min=1.0, + thunder=0, + total_cloud=100, + valid_time=datetime(2018, 1, 2, 0, 0, 0), + visibility=8.8, + wind_direction=114, + wind_gust=5.8, + wind_speed=2.5, ) testdata = [data, data2, data3] - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) with ( patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", return_value=testdata, ), patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast_hour", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_hourly_forecast", return_value=None, ), ): @@ -232,12 +249,12 @@ async def test_refresh_weather_forecast_retry( hass: HomeAssistant, error: Exception ) -> None: """Test the refresh weather forecast function.""" - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) now = dt_util.utcnow() with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + "homeassistant.components.smhi.weather.SMHIPointForecast.async_get_daily_forecast", side_effect=error, ) as mock_get_forecast: await hass.config_entries.async_setup(entry.entry_id) @@ -352,12 +369,12 @@ async def test_custom_speed_unit( api_response: str, ) -> None: """Test Wind Gust speed with custom unit.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -389,12 +406,12 @@ async def test_forecast_services( snapshot: SnapshotAssertion, ) -> None: """Test multiple forecast.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -440,7 +457,7 @@ async def test_forecast_services( assert msg["type"] == "event" forecast1 = msg["event"]["forecast"] - assert len(forecast1) == 72 + assert len(forecast1) == 52 assert forecast1[0] == snapshot assert forecast1[6] == snapshot @@ -453,12 +470,12 @@ async def test_forecast_services_lack_of_data( snapshot: SnapshotAssertion, ) -> None: """Test forecast lacking data.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response_lack_data) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -498,12 +515,12 @@ async def test_forecast_service( service: str, ) -> None: """Test forecast service.""" - uri = APIURL_TEMPLATE.format( + uri = API_POINT_FORECAST.format( TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) + entry = MockConfigEntry(domain="smhi", title="test", data=TEST_CONFIG, version=3) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/smlight/conftest.py b/tests/components/smlight/conftest.py index 665a55ba880..7a1b16f1d6b 100644 --- a/tests/components/smlight/conftest.py +++ b/tests/components/smlight/conftest.py @@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch +from pysmlight.exceptions import SmlightAuthError from pysmlight.sse import sseClient from pysmlight.web import CmdWrapper, Firmware, Info, Sensors import pytest @@ -18,7 +19,8 @@ from tests.common import ( load_json_object_fixture, ) -MOCK_HOST = "slzb-06.local" +MOCK_DEVICE_NAME = "slzb-06" +MOCK_HOST = "192.168.1.161" MOCK_USERNAME = "test-user" MOCK_PASSWORD = "test-pass" @@ -80,9 +82,16 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]: ): api = smlight_mock.return_value api.host = MOCK_HOST - api.get_info.return_value = Info.from_dict( - load_json_object_fixture("info.json", DOMAIN) - ) + + def get_info_side_effect(*args, **kwargs) -> Info: + """Return the info.""" + if api.check_auth_needed.return_value and not api.authenticate.called: + raise SmlightAuthError + + return Info.from_dict(load_json_object_fixture("info.json", DOMAIN)) + + api.get_info.side_effect = get_info_side_effect + api.get_sensors.return_value = Sensors.from_dict( load_json_object_fixture("sensors.json", DOMAIN) ) @@ -91,7 +100,10 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]: """Return the firmware version.""" fw_list = [] if kwargs.get("mode") == "zigbee": - fw_list = load_json_array_fixture("zb_firmware.json", DOMAIN) + if kwargs.get("zb_type") == 0: + fw_list = load_json_array_fixture("zb_firmware.json", DOMAIN) + else: + fw_list = load_json_array_fixture("zb_firmware_router.json", DOMAIN) else: fw_list = load_json_array_fixture("esp_firmware.json", DOMAIN) diff --git a/tests/components/smlight/fixtures/esp_firmware.json b/tests/components/smlight/fixtures/esp_firmware.json index 6ea0e1a8b44..f0ee9eb989a 100644 --- a/tests/components/smlight/fixtures/esp_firmware.json +++ b/tests/components/smlight/fixtures/esp_firmware.json @@ -2,10 +2,10 @@ { "mode": "ESP", "type": null, - "notes": "CHANGELOG (Current 2.5.2 vs. Previous 2.3.6):\\r\\nFixed incorrect device type detection for some devices\\r\\nFixed web interface not working on some devices\\r\\nFixed disabled SSID/pass fields\\r\\n", + "notes": "CHANGELOG (Current 2.7.5 vs. Previous 2.3.6):\\r\\nFixed incorrect device type detection for some devices\\r\\nFixed web interface not working on some devices\\r\\nFixed disabled SSID/pass fields\\r\\n", "rev": "20240830", "link": "https://smlight.tech/flasher/firmware/bin/slzb06x/core/slzb-06-v2.5.2-ota.bin", - "ver": "v2.5.2", + "ver": "v2.7.5", "dev": false, "prod": true, "baud": null diff --git a/tests/components/smlight/fixtures/info-2.3.6.json b/tests/components/smlight/fixtures/info-2.3.6.json new file mode 100644 index 00000000000..e3defb4410e --- /dev/null +++ b/tests/components/smlight/fixtures/info-2.3.6.json @@ -0,0 +1,19 @@ +{ + "coord_mode": 0, + "device_ip": "192.168.1.161", + "fs_total": 3456, + "fw_channel": "dev", + "legacy_api": 0, + "hostname": "SLZB-06p7", + "MAC": "AA:BB:CC:DD:EE:FF", + "model": "SLZB-06p7", + "ram_total": 296, + "sw_version": "v2.3.6", + "wifi_mode": 0, + "zb_flash_size": 704, + "zb_channel": 0, + "zb_hw": "CC2652P7", + "zb_ram_size": 152, + "zb_version": "20240314", + "zb_type": 0 +} diff --git a/tests/components/smlight/fixtures/info-MR1.json b/tests/components/smlight/fixtures/info-MR1.json new file mode 100644 index 00000000000..df1c0b0f789 --- /dev/null +++ b/tests/components/smlight/fixtures/info-MR1.json @@ -0,0 +1,41 @@ +{ + "coord_mode": 0, + "device_ip": "192.168.1.161", + "fs_total": 3456, + "fw_channel": "dev", + "legacy_api": 0, + "hostname": "SLZB-MR1", + "MAC": "AA:BB:CC:DD:EE:FF", + "model": "SLZB-MR1", + "ram_total": 296, + "sw_version": "v2.7.3", + "wifi_mode": 0, + "zb_flash_size": 704, + "zb_channel": 0, + "zb_hw": "CC2652P7", + "zb_ram_size": 152, + "zb_version": "20240314", + "zb_type": 0, + "radios": [ + { + "chip_index": 0, + "zb_hw": "EFR32MG21", + "zb_version": 20241127, + "zb_type": 0, + "zb_channel": 0, + "zb_ram_size": 152, + "zb_flash_size": 704, + "radioModes": [true, true, true, false, false] + }, + { + "chip_index": 1, + "zb_hw": "CC2652P7", + "zb_version": 20240314, + "zb_type": 1, + "zb_channel": 0, + "zb_ram_size": 152, + "zb_flash_size": 704, + "radioModes": [true, true, true, false, false] + } + ] +} diff --git a/tests/components/smlight/fixtures/info.json b/tests/components/smlight/fixtures/info.json index e3defb4410e..b94fdc3d61c 100644 --- a/tests/components/smlight/fixtures/info.json +++ b/tests/components/smlight/fixtures/info.json @@ -15,5 +15,17 @@ "zb_hw": "CC2652P7", "zb_ram_size": 152, "zb_version": "20240314", - "zb_type": 0 + "zb_type": 0, + "radios": [ + { + "chip_index": 0, + "zb_hw": "CC2652P7", + "zb_version": "20240314", + "zb_type": 0, + "zb_channel": 0, + "zb_ram_size": 152, + "zb_flash_size": 704, + "radioModes": [true, true, true, false, false] + } + ] } diff --git a/tests/components/smlight/fixtures/zb_firmware.json b/tests/components/smlight/fixtures/zb_firmware.json index ca9d10f87ac..b35bb20d64e 100644 --- a/tests/components/smlight/fixtures/zb_firmware.json +++ b/tests/components/smlight/fixtures/zb_firmware.json @@ -3,24 +3,13 @@ "mode": "ZB", "type": 0, "notes": "SMLIGHT latest Coordinator release for CC2674P10 chips [16-Jul-2024]:
- +20dB TRANSMIT POWER SUPPORT;
- SDK 7.41 based (latest);
", - "rev": "20240716", + "rev": "20250201", "link": "https://smlight.tech/flasher/firmware/bin/slzb06x/zigbee/slzb06p10/znp-SLZB-06P10-20240716.bin", - "ver": "20240716", + "ver": "20250201", "dev": false, "prod": true, "baud": 115200 }, - { - "mode": "ZB", - "type": 1, - "notes": "SMLIGHT latest ROUTER release for CC2674P10 chips [16-Jul-2024]:
- SDK 7.41 based (latest);
Terms of use", - "rev": "20240716", - "link": "https://smlight.tech/flasher/firmware/bin/slzb06x/zigbee/slzb06p10/zr-ZR_SLZB-06P10-20240716.bin", - "ver": "20240716", - "dev": false, - "prod": true, - "baud": 0 - }, { "mode": "ZB", "type": 0, diff --git a/tests/components/smlight/fixtures/zb_firmware_router.json b/tests/components/smlight/fixtures/zb_firmware_router.json new file mode 100644 index 00000000000..320fef89347 --- /dev/null +++ b/tests/components/smlight/fixtures/zb_firmware_router.json @@ -0,0 +1,13 @@ +[ + { + "mode": "ZB", + "type": 1, + "notes": "SMLIGHT latest ROUTER release for CC2652P7 chips [16-Jul-2024]:
- SDK 7.41 based (latest);
Terms of use - by downloading and installing this firmware, you agree to the aforementioned terms.", + "rev": "20240716", + "link": "https://smlight.tech/flasher/firmware/bin/slzb06x/zigbee/slzb06p10/znp-SLZB-06P10-20240716.bin", + "ver": "20240716", + "dev": false, + "prod": true, + "baud": 115200 + } +] diff --git a/tests/components/smlight/snapshots/test_binary_sensor.ambr b/tests/components/smlight/snapshots/test_binary_sensor.ambr index 8becf5b2567..edb2a914a5d 100644 --- a/tests/components/smlight/snapshots/test_binary_sensor.ambr +++ b/tests/components/smlight/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smlight/snapshots/test_diagnostics.ambr b/tests/components/smlight/snapshots/test_diagnostics.ambr index 97177de1704..5ee6cd19676 100644 --- a/tests/components/smlight/snapshots/test_diagnostics.ambr +++ b/tests/components/smlight/snapshots/test_diagnostics.ambr @@ -10,6 +10,24 @@ 'hostname': 'SLZB-06p7', 'legacy_api': 0, 'model': 'SLZB-06p7', + 'radios': list([ + dict({ + 'chip_index': 0, + 'radioModes': list([ + True, + True, + True, + False, + False, + ]), + 'zb_channel': 0, + 'zb_flash_size': 704, + 'zb_hw': 'CC2652P7', + 'zb_ram_size': 152, + 'zb_type': 0, + 'zb_version': '20240314', + }), + ]), 'ram_total': 296, 'sw_version': 'v2.3.6', 'wifi_mode': 0, diff --git a/tests/components/smlight/snapshots/test_init.ambr b/tests/components/smlight/snapshots/test_init.ambr index 598166e537b..ba374199254 100644 --- a/tests/components/smlight/snapshots/test_init.ambr +++ b/tests/components/smlight/snapshots/test_init.ambr @@ -3,7 +3,8 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , - 'configuration_url': 'http://slzb-06.local', + 'config_entries_subentries': , + 'configuration_url': 'http://192.168.1.161', 'connections': set({ tuple( 'mac', diff --git a/tests/components/smlight/snapshots/test_sensor.ambr b/tests/components/smlight/snapshots/test_sensor.ambr index 262ecfe1544..542338e4dbf 100644 --- a/tests/components/smlight/snapshots/test_sensor.ambr +++ b/tests/components/smlight/snapshots/test_sensor.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -118,6 +120,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -165,6 +168,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -218,6 +222,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -269,6 +274,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -319,6 +325,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -377,6 +384,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -429,6 +437,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smlight/snapshots/test_switch.ambr b/tests/components/smlight/snapshots/test_switch.ambr index 733d002be0f..b748202a557 100644 --- a/tests/components/smlight/snapshots/test_switch.ambr +++ b/tests/components/smlight/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/smlight/snapshots/test_update.ambr b/tests/components/smlight/snapshots/test_update.ambr index ed0085dcdc8..dc6b8f46ca5 100644 --- a/tests/components/smlight/snapshots/test_update.ambr +++ b/tests/components/smlight/snapshots/test_update.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -42,7 +43,7 @@ 'friendly_name': 'Mock Title Core firmware', 'in_progress': False, 'installed_version': 'v2.3.6', - 'latest_version': 'v2.5.2', + 'latest_version': 'v2.7.5', 'release_summary': None, 'release_url': None, 'skipped_version': None, @@ -65,6 +66,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -101,7 +103,7 @@ 'friendly_name': 'Mock Title Zigbee firmware', 'in_progress': False, 'installed_version': '20240314', - 'latest_version': '20240716', + 'latest_version': '20250201', 'release_summary': None, 'release_url': None, 'skipped_version': None, diff --git a/tests/components/smlight/test_button.py b/tests/components/smlight/test_button.py index 3721ee815e6..51e9414c00e 100644 --- a/tests/components/smlight/test_button.py +++ b/tests/components/smlight/test_button.py @@ -45,6 +45,7 @@ async def test_buttons( mock_smlight_client: MagicMock, ) -> None: """Test creation of button entities.""" + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = MOCK_ROUTER await setup_integration(hass, mock_config_entry) @@ -78,6 +79,7 @@ async def test_disabled_by_default_buttons( mock_smlight_client: MagicMock, ) -> None: """Test the disabled by default buttons.""" + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = MOCK_ROUTER await setup_integration(hass, mock_config_entry) @@ -96,7 +98,8 @@ async def test_remove_router_reconnect( mock_smlight_client: MagicMock, ) -> None: """Test removal of orphaned router reconnect button.""" - save_mock = mock_smlight_client.get_info.return_value + save_mock = mock_smlight_client.get_info.side_effect + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = MOCK_ROUTER mock_config_entry = await setup_integration(hass, mock_config_entry) @@ -106,7 +109,7 @@ async def test_remove_router_reconnect( assert len(entities) == 4 assert entities[3].unique_id == "aa:bb:cc:dd:ee:ff-reconnect_zigbee_router" - mock_smlight_client.get_info.return_value = save_mock + mock_smlight_client.get_info.side_effect = save_mock freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) diff --git a/tests/components/smlight/test_config_flow.py b/tests/components/smlight/test_config_flow.py index 2fd39f75704..c8933029ce6 100644 --- a/tests/components/smlight/test_config_flow.py +++ b/tests/components/smlight/test_config_flow.py @@ -3,23 +3,25 @@ from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock +from pysmlight import Info from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError import pytest -from homeassistant.components import zeroconf from homeassistant.components.smlight.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .conftest import MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME +from .conftest import MOCK_DEVICE_NAME, MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME from tests.common import MockConfigEntry -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("127.0.0.1"), - ip_addresses=[ip_address("127.0.0.1")], +DISCOVERY_INFO = ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.161"), + ip_addresses=[ip_address("192.168.1.161")], hostname="slzb-06.local.", name="mock_name", port=6638, @@ -27,9 +29,9 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( type="mock_type", ) -DISCOVERY_INFO_LEGACY = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("127.0.0.1"), - ip_addresses=[ip_address("127.0.0.1")], +DISCOVERY_INFO_LEGACY = ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.161"), + ip_addresses=[ip_address("192.168.1.161")], hostname="slzb-06.local.", name="mock_name", port=6638, @@ -51,7 +53,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: MOCK_HOST, + CONF_HOST: "slzb-06p7.local", }, ) @@ -64,6 +66,46 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_flow_auth( + hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock +) -> None: + """Test the full manual user flow with authentication.""" + + mock_smlight_client.check_auth_needed.return_value = True + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "slzb-06p7.local", + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "auth" + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + }, + ) + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "SLZB-06p7" + assert result3["data"] == { + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_HOST: MOCK_HOST, + } + assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_flow( hass: HomeAssistant, mock_smlight_client: MagicMock, @@ -75,7 +117,7 @@ async def test_zeroconf_flow( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO ) - assert result["description_placeholders"] == {"host": MOCK_HOST} + assert result["description_placeholders"] == {"host": MOCK_DEVICE_NAME} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" @@ -97,7 +139,7 @@ async def test_zeroconf_flow( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_smlight_client.get_info.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 2 async def test_zeroconf_flow_auth( @@ -112,7 +154,7 @@ async def test_zeroconf_flow_auth( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO ) - assert result["description_placeholders"] == {"host": MOCK_HOST} + assert result["description_placeholders"] == {"host": MOCK_DEVICE_NAME} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" @@ -143,7 +185,7 @@ async def test_zeroconf_flow_auth( assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["context"]["source"] == "zeroconf" assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" - assert result3["title"] == "slzb-06" + assert result3["title"] == "SLZB-06p7" assert result3["data"] == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, @@ -151,12 +193,99 @@ async def test_zeroconf_flow_auth( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_smlight_client.get_info.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 3 + + +async def test_zeroconf_unsupported_abort( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test we abort zeroconf flow if device unsupported.""" + mock_smlight_client.get_info.side_effect = None + mock_smlight_client.get_info.return_value = Info(model="SLZB-X") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO + ) + + assert result["description_placeholders"] == {"host": MOCK_DEVICE_NAME} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "unsupported_device" + + +async def test_user_unsupported_abort( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test we abort user flow if unsupported device.""" + mock_smlight_client.get_info.side_effect = None + mock_smlight_client.get_info.return_value = Info(model="SLZB-X") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: MOCK_HOST, + }, + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "unsupported_device" + + +async def test_user_unsupported_device_abort_auth( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test we abort user flow if unsupported device (with auth).""" + mock_smlight_client.check_auth_needed.return_value = True + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: MOCK_HOST, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + + mock_smlight_client.get_info.side_effect = None + mock_smlight_client.get_info.return_value = Info(model="SLZB-X") + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + }, + ) + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "unsupported_device" @pytest.mark.usefixtures("mock_smlight_client") async def test_user_device_exists_abort( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test we abort user flow if device already configured.""" mock_config_entry.add_to_hass(hass) @@ -173,6 +302,44 @@ async def test_user_device_exists_abort( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_smlight_client") +async def test_user_flow_can_override_discovery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test manual user flow can override discovery in progress.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: MOCK_HOST, + }, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["context"]["source"] == SOURCE_USER + assert result2["data"] == { + CONF_HOST: MOCK_HOST, + } + assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.usefixtures("mock_smlight_client") async def test_zeroconf_device_exists_abort( hass: HomeAssistant, mock_config_entry: MockConfigEntry @@ -239,7 +406,7 @@ async def test_user_invalid_auth( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_smlight_client.get_info.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 3 async def test_user_cannot_connect( @@ -276,7 +443,7 @@ async def test_user_cannot_connect( assert result2["title"] == "SLZB-06p7" assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_smlight_client.get_info.mock_calls) == 1 + assert len(mock_smlight_client.get_info.mock_calls) == 3 async def test_auth_cannot_connect( @@ -363,7 +530,7 @@ async def test_zeroconf_legacy_mac( data=DISCOVERY_INFO_LEGACY, ) - assert result["description_placeholders"] == {"host": MOCK_HOST} + assert result["description_placeholders"] == {"host": MOCK_DEVICE_NAME} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -378,7 +545,77 @@ async def test_zeroconf_legacy_mac( } assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_smlight_client.get_info.mock_calls) == 2 + assert len(mock_smlight_client.get_info.mock_calls) == 3 + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_zeroconf_updates_host( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test zeroconf discovery updates host ip.""" + mock_config_entry.add_to_hass(hass) + + service_info = DISCOVERY_INFO + service_info.ip_address = ip_address("192.168.1.164") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "192.168.1.164" + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_dhcp_discovery_updates_host( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test dhcp discovery updates host ip.""" + mock_config_entry.add_to_hass(hass) + + service_info = DhcpServiceInfo( + ip="192.168.1.164", + hostname="slzb-06", + macaddress="aabbccddeeff", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "192.168.1.164" + + +@pytest.mark.usefixtures("mock_smlight_client") +async def test_dhcp_discovery_aborts( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test dhcp discovery updates host ip.""" + mock_config_entry.add_to_hass(hass) + + service_info = DhcpServiceInfo( + ip="192.168.1.161", + hostname="slzb-06", + macaddress="000000000000", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "192.168.1.161" async def test_reauth_flow( diff --git a/tests/components/smlight/test_init.py b/tests/components/smlight/test_init.py index afc53932fb0..692255a53e6 100644 --- a/tests/components/smlight/test_init.py +++ b/tests/components/smlight/test_init.py @@ -8,9 +8,14 @@ from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError, Smlig import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.smlight.const import DOMAIN, SCAN_INTERVAL +from homeassistant.components.smlight.const import ( + DOMAIN, + SCAN_FIRMWARE_INTERVAL, + SCAN_INTERVAL, +) +from homeassistant.components.update import ATTR_INSTALLED_VERSION from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.issue_registry import IssueRegistry @@ -73,6 +78,42 @@ async def test_async_setup_missing_credentials( assert progress[0]["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" +async def test_async_setup_no_internet( + hass: HomeAssistant, + mock_config_entry_host: MockConfigEntry, + mock_smlight_client: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we still load integration when no internet is available.""" + side_effect = mock_smlight_client.get_firmware_version.side_effect + mock_smlight_client.get_firmware_version.side_effect = SmlightConnectionError + + await setup_integration(hass, mock_config_entry_host) + + entity = hass.states.get("update.mock_title_core_firmware") + assert entity is not None + assert entity.state == STATE_UNKNOWN + + freezer.tick(SCAN_FIRMWARE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity = hass.states.get("update.mock_title_core_firmware") + assert entity is not None + assert entity.state == STATE_UNKNOWN + + mock_smlight_client.get_firmware_version.side_effect = side_effect + + freezer.tick(SCAN_FIRMWARE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + entity = hass.states.get("update.mock_title_core_firmware") + assert entity is not None + assert entity.state == STATE_ON + assert entity.attributes[ATTR_INSTALLED_VERSION] == "v2.3.6" + + @pytest.mark.parametrize("error", [SmlightConnectionError, SmlightAuthError]) async def test_update_failed( hass: HomeAssistant, @@ -124,6 +165,7 @@ async def test_device_legacy_firmware( """Test device setup for old firmware version that dont support required API.""" LEGACY_VERSION = "v0.9.9" mock_smlight_client.get_sensors.side_effect = SmlightError + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info( legacy_api=2, sw_version=LEGACY_VERSION, MAC="AA:BB:CC:DD:EE:FF" ) diff --git a/tests/components/smlight/test_update.py b/tests/components/smlight/test_update.py index 0bb2e34d7ca..86d19968910 100644 --- a/tests/components/smlight/test_update.py +++ b/tests/components/smlight/test_update.py @@ -4,13 +4,13 @@ from datetime import timedelta from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory -from pysmlight import Firmware, Info +from pysmlight import Firmware, Info, Radio from pysmlight.const import Events as SmEvents from pysmlight.sse import MessageEvent import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.smlight.const import SCAN_FIRMWARE_INTERVAL +from homeassistant.components.smlight.const import DOMAIN, SCAN_FIRMWARE_INTERVAL from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -27,7 +27,12 @@ from homeassistant.helpers import entity_registry as er from . import get_mock_event_function from .conftest import setup_integration -from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, + snapshot_platform, +) from tests.typing import WebSocketGenerator pytestmark = [ @@ -62,12 +67,14 @@ MOCK_FIRMWARE_FAIL = MessageEvent( MOCK_FIRMWARE_NOTES = [ Firmware( - ver="v2.3.6", + ver="v2.7.2", mode="ESP", notes=None, ) ] +MOCK_RADIO = Radio(chip_index=1, zb_channel=0, zb_type=0, zb_version="20240716") + @pytest.fixture def platforms() -> list[Platform]: @@ -81,7 +88,7 @@ async def test_update_setup( mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: - """Test setup of SMLIGHT switches.""" + """Test setup of SMLIGHT update entities.""" entry = await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) @@ -103,7 +110,7 @@ async def test_update_firmware( state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.3.6" - assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.7.5" await hass.services.async_call( PLATFORM, @@ -125,8 +132,9 @@ async def test_update_firmware( event_function(MOCK_FIRMWARE_DONE) + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info( - sw_version="v2.5.2", + sw_version="v2.7.5", ) freezer.tick(timedelta(seconds=5)) @@ -135,8 +143,51 @@ async def test_update_firmware( state = hass.states.get(entity_id) assert state.state == STATE_OFF - assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.5.2" - assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.7.5" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.7.5" + + +async def test_update_zigbee2_firmware( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test update of zigbee2 firmware where available.""" + mock_smlight_client.get_info.side_effect = None + mock_smlight_client.get_info.return_value = Info.from_dict( + load_json_object_fixture("info-MR1.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + entity_id = "update.mock_title_zigbee_firmware_2" + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "20240314" + assert state.attributes[ATTR_LATEST_VERSION] == "20240716" + + await hass.services.async_call( + PLATFORM, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: entity_id}, + blocking=False, + ) + + assert len(mock_smlight_client.fw_update.mock_calls) == 1 + + event_function = get_mock_event_function(mock_smlight_client, SmEvents.FW_UPD_done) + + event_function(MOCK_FIRMWARE_DONE) + with patch( + "homeassistant.components.smlight.update.get_radio", return_value=MOCK_RADIO + ): + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state.attributes[ATTR_INSTALLED_VERSION] == "20240716" + assert state.attributes[ATTR_LATEST_VERSION] == "20240716" async def test_update_legacy_firmware_v2( @@ -146,6 +197,7 @@ async def test_update_legacy_firmware_v2( mock_smlight_client: MagicMock, ) -> None: """Test firmware update for legacy v2 firmware.""" + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info( sw_version="v2.0.18", legacy_api=1, @@ -156,7 +208,7 @@ async def test_update_legacy_firmware_v2( state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.0.18" - assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.7.5" await hass.services.async_call( PLATFORM, @@ -171,8 +223,9 @@ async def test_update_legacy_firmware_v2( event_function(MOCK_FIRMWARE_DONE) + mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info( - sw_version="v2.5.2", + sw_version="v2.7.5", ) freezer.tick(SCAN_FIRMWARE_INTERVAL) @@ -181,8 +234,8 @@ async def test_update_legacy_firmware_v2( state = hass.states.get(entity_id) assert state.state == STATE_OFF - assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.5.2" - assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.7.5" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.7.5" async def test_update_firmware_failed( @@ -196,7 +249,7 @@ async def test_update_firmware_failed( state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.3.6" - assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.7.5" await hass.services.async_call( PLATFORM, @@ -233,7 +286,7 @@ async def test_update_reboot_timeout( state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_INSTALLED_VERSION] == "v2.3.6" - assert state.attributes[ATTR_LATEST_VERSION] == "v2.5.2" + assert state.attributes[ATTR_LATEST_VERSION] == "v2.7.5" with ( patch( @@ -267,18 +320,30 @@ async def test_update_reboot_timeout( mock_warning.assert_called_once() +@pytest.mark.parametrize( + "entity_id", + [ + "update.mock_title_core_firmware", + "update.mock_title_zigbee_firmware", + "update.mock_title_zigbee_firmware_2", + ], +) async def test_update_release_notes( hass: HomeAssistant, + entity_id: str, freezer: FrozenDateTimeFactory, mock_config_entry: MockConfigEntry, mock_smlight_client: MagicMock, hass_ws_client: WebSocketGenerator, ) -> None: """Test firmware release notes.""" + mock_smlight_client.get_info.side_effect = None + mock_smlight_client.get_info.return_value = Info.from_dict( + load_json_object_fixture("info-MR1.json", DOMAIN) + ) await setup_integration(hass, mock_config_entry) ws_client = await hass_ws_client(hass) await hass.async_block_till_done() - entity_id = "update.mock_title_core_firmware" state = hass.states.get(entity_id) assert state @@ -294,16 +359,30 @@ async def test_update_release_notes( result = await ws_client.receive_json() assert result["result"] is not None + +async def test_update_blank_release_notes( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test firmware missing release notes.""" + + entity_id = "update.mock_title_core_firmware" mock_smlight_client.get_firmware_version.side_effect = None mock_smlight_client.get_firmware_version.return_value = MOCK_FIRMWARE_NOTES - freezer.tick(SCAN_FIRMWARE_INTERVAL) - async_fire_time_changed(hass) + await setup_integration(hass, mock_config_entry) + ws_client = await hass_ws_client(hass) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + await ws_client.send_json( { - "id": 2, + "id": 1, "type": "update/release_notes", "entity_id": entity_id, } diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index 06bc01f9d39..c51f7627efc 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -116,6 +118,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -173,6 +176,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -224,6 +228,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -275,6 +280,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -329,6 +335,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -380,6 +387,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -437,6 +445,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -494,6 +503,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -551,6 +561,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -606,6 +617,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -662,6 +674,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -716,6 +729,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -765,6 +779,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -814,6 +829,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -865,6 +881,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -916,6 +933,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -967,6 +985,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1018,6 +1037,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1072,6 +1092,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1123,6 +1144,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1174,6 +1196,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1231,6 +1254,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1288,6 +1312,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1345,6 +1370,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1397,6 +1423,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 9084d988ec9..b7007f27fa9 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.somfy_mylink.const import ( CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, @@ -14,6 +13,7 @@ from homeassistant.components.somfy_mylink.const import ( from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -263,7 +263,7 @@ async def test_form_user_already_configured_from_dhcp(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbccddeeff", hostname="somfy_eeff", @@ -287,7 +287,7 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbccddeeff", hostname="somfy_eeff", @@ -302,7 +302,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbccddeeff", hostname="somfy_eeff", diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 5215e9b3c0e..0ae2ab596db 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -4,7 +4,6 @@ import copy import dataclasses from unittest.mock import patch -from homeassistant.components import ssdp from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN from homeassistant.config_entries import ( SOURCE_IMPORT, @@ -15,6 +14,11 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from . import ( CONF_DATA, @@ -30,13 +34,13 @@ from tests.common import MockConfigEntry UDN = "uuid:1234" -SSDP_DATA = ssdp.SsdpServiceInfo( +SSDP_DATA = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{HOST}:52323/dmr.xml", upnp={ - ssdp.ATTR_UPNP_UDN: UDN, - ssdp.ATTR_UPNP_FRIENDLY_NAME: FRIENDLY_NAME, + ATTR_UPNP_UDN: UDN, + ATTR_UPNP_FRIENDLY_NAME: FRIENDLY_NAME, "X_ScalarWebAPI_DeviceInfo": { "X_ScalarWebAPI_BaseURL": ENDPOINT, "X_ScalarWebAPI_ServiceList": { diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 04b35e2c021..0f56794b9f2 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -18,11 +18,13 @@ from soco.data_structures import ( ) from soco.events_base import Event as SonosEvent -from homeassistant.components import ssdp, zeroconf +from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture, load_json_value_fixture @@ -108,7 +110,7 @@ class SonosMockEvent: @pytest.fixture def zeroconf_payload(): """Return a default zeroconf payload.""" - return zeroconf.ZeroconfServiceInfo( + return ZeroconfServiceInfo( ip_address=ip_address("192.168.4.2"), ip_addresses=[ip_address("192.168.4.2")], hostname="Sonos-aaa", @@ -335,17 +337,17 @@ def discover_fixture(soco): def do_callback( hass: HomeAssistant, callback: Callable[ - [ssdp.SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None + [SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None ], match_dict: dict[str, str] | None = None, ) -> MagicMock: callback( - ssdp.SsdpServiceInfo( + SsdpServiceInfo( ssdp_location=f"http://{soco.ip_address}/", ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1", ssdp_usn=f"uuid:{soco.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1", upnp={ - ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", + ATTR_UPNP_UDN: f"uuid:{soco.uid}", }, ), ssdp.SsdpChange.ALIVE, diff --git a/tests/components/sonos/snapshots/test_media_player.ambr b/tests/components/sonos/snapshots/test_media_player.ambr index 8ef298de3db..7f4681d8915 100644 --- a/tests/components/sonos/snapshots/test_media_player.ambr +++ b/tests/components/sonos/snapshots/test_media_player.ambr @@ -7,6 +7,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 141013dec20..70605092da1 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -6,17 +6,18 @@ from ipaddress import ip_address from unittest.mock import MagicMock, patch from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component async def test_user_form( - hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo + hass: HomeAssistant, zeroconf_payload: ZeroconfServiceInfo ) -> None: """Test we get the user initiated form.""" @@ -84,7 +85,7 @@ async def test_user_form_already_created(hass: HomeAssistant) -> None: async def test_zeroconf_form( - hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo + hass: HomeAssistant, zeroconf_payload: ZeroconfServiceInfo ) -> None: """Test we pass Zeroconf discoveries to the manager.""" @@ -128,12 +129,12 @@ async def test_ssdp_discovery(hass: HomeAssistant, soco) -> None: await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_location=f"http://{soco.ip_address}/", ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1", ssdp_usn=f"uuid:{soco.uid}_MR::urn:schemas-upnp-org:service:GroupRenderingControl:1", upnp={ - ssdp.ATTR_UPNP_UDN: f"uuid:{soco.uid}", + ATTR_UPNP_UDN: f"uuid:{soco.uid}", }, ), ) @@ -173,7 +174,7 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.107"), ip_addresses=[ip_address("192.168.1.107")], port=1443, @@ -221,7 +222,7 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: async def test_zeroconf_form_not_sonos( - hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo + hass: HomeAssistant, zeroconf_payload: ZeroconfServiceInfo ) -> None: """Test we abort on non-sonos devices.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 36a6571f3b0..a7ad2f4cb82 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries -from homeassistant.components import sonos, zeroconf +from homeassistant.components import sonos from homeassistant.components.sonos import SonosDiscoveryManager from homeassistant.components.sonos.const import ( DATA_SONOS_DISCOVERY_MANAGER, @@ -19,8 +19,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import MockSoCo, SoCoMockFactory @@ -28,7 +29,7 @@ from tests.common import async_fire_time_changed async def test_creating_entry_sets_up_media_player( - hass: HomeAssistant, zeroconf_payload: zeroconf.ZeroconfServiceInfo + hass: HomeAssistant, zeroconf_payload: ZeroconfServiceInfo ) -> None: """Test setting up Sonos loads the media player.""" diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py index 264049ab5fc..fe524da5603 100644 --- a/tests/components/soundtouch/test_config_flow.py +++ b/tests/components/soundtouch/test_config_flow.py @@ -8,11 +8,11 @@ import requests_mock from requests_mock import ANY, Mocker from homeassistant.components.soundtouch.const import DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .conftest import DEVICE_1_ID, DEVICE_1_IP, DEVICE_1_NAME diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 2e20aaa259c..1dd30c425b3 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -16,7 +16,7 @@ from homeassistant.components.speedtestdotnet.coordinator import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/spotify/snapshots/test_media_player.ambr b/tests/components/spotify/snapshots/test_media_player.ambr index 9692d59cfd1..74dbcb50f92 100644 --- a/tests/components/spotify/snapshots/test_media_player.ambr +++ b/tests/components/spotify/snapshots/test_media_player.ambr @@ -10,6 +10,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -79,6 +80,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index cb942a63568..24c0e1d41d9 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -7,18 +7,18 @@ from unittest.mock import MagicMock, patch import pytest from spotifyaio import SpotifyConnectionError -from homeassistant.components import zeroconf from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator -BLANK_ZEROCONF_INFO = zeroconf.ZeroconfServiceInfo( +BLANK_ZEROCONF_INFO = ZeroconfServiceInfo( ip_address=ip_address("1.2.3.4"), ip_addresses=[ip_address("1.2.3.4")], hostname="mock_hostname", diff --git a/tests/components/spotify/test_media_player.py b/tests/components/spotify/test_media_player.py index 55e0ea8f1d8..456af43d411 100644 --- a/tests/components/spotify/test_media_player.py +++ b/tests/components/spotify/test_media_player.py @@ -641,3 +641,147 @@ async def test_no_album_images( state = hass.states.get("media_player.spotify_spotify_1") assert state assert ATTR_ENTITY_PICTURE not in state.attributes + + +@pytest.mark.usefixtures("setup_credentials") +async def test_normal_polling_interval( + hass: HomeAssistant, + mock_spotify: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Spotify media player polling interval.""" + await setup_integration(hass, mock_config_entry) + + assert mock_spotify.return_value.get_playback.return_value.is_playing is True + assert ( + mock_spotify.return_value.get_playback.return_value.progress_ms + - mock_spotify.return_value.get_playback.return_value.item.duration_ms + < 30000 + ) + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_called_once() + + +@pytest.mark.usefixtures("setup_credentials") +async def test_smart_polling_interval( + hass: HomeAssistant, + mock_spotify: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Spotify media player polling interval.""" + freezer.move_to("2023-10-21") + mock_spotify.return_value.get_playback.return_value.progress_ms = 10000 + mock_spotify.return_value.get_playback.return_value.item.duration_ms = 30000 + + await setup_integration(hass, mock_config_entry) + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + freezer.tick(timedelta(seconds=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_not_called() + + mock_spotify.return_value.get_playback.return_value.progress_ms = 10000 + mock_spotify.return_value.get_playback.return_value.item.duration_ms = 50000 + + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + freezer.tick(timedelta(seconds=21)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_not_called() + + freezer.tick(timedelta(seconds=9)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + +@pytest.mark.usefixtures("setup_credentials") +async def test_smart_polling_interval_handles_errors( + hass: HomeAssistant, + mock_spotify: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Spotify media player polling interval.""" + mock_spotify.return_value.get_playback.return_value.progress_ms = 10000 + mock_spotify.return_value.get_playback.return_value.item.duration_ms = 30000 + + await setup_integration(hass, mock_config_entry) + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + mock_spotify.return_value.get_playback.side_effect = SpotifyConnectionError + + freezer.tick(timedelta(seconds=21)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + freezer.tick(timedelta(seconds=21)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_not_called() + + freezer.tick(timedelta(seconds=9)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + +@pytest.mark.usefixtures("setup_credentials") +async def test_smart_polling_interval_handles_paused( + hass: HomeAssistant, + mock_spotify: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Spotify media player polling interval.""" + mock_spotify.return_value.get_playback.return_value.progress_ms = 10000 + mock_spotify.return_value.get_playback.return_value.item.duration_ms = 30000 + mock_spotify.return_value.get_playback.return_value.is_playing = False + + await setup_integration(hass, mock_config_entry) + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() + + freezer.tick(timedelta(seconds=21)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_not_called() + + freezer.tick(timedelta(seconds=9)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playback.assert_called_once() + mock_spotify.return_value.get_playback.reset_mock() diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 2dc0cabeaa6..7b007114420 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -137,6 +137,7 @@ async def mock_async_browse( "title": "Fake Item 1", "id": FAKE_VALID_ITEM_ID, "hasitems": False, + "isaudio": True, "item_type": child_types[media_type], "artwork_track_id": "b35bb9e9", "url": "file:///var/lib/squeezeboxserver/music/track_1.mp3", @@ -145,6 +146,7 @@ async def mock_async_browse( "title": "Fake Item 2", "id": FAKE_VALID_ITEM_ID + "_2", "hasitems": media_type == "favorites", + "isaudio": True, "item_type": child_types[media_type], "image_url": "http://lms.internal:9000/html/images/favorites.png", "url": "file:///var/lib/squeezeboxserver/music/track_2.mp3", @@ -153,6 +155,7 @@ async def mock_async_browse( "title": "Fake Item 3", "id": FAKE_VALID_ITEM_ID + "_3", "hasitems": media_type == "favorites", + "isaudio": True, "album_id": FAKE_VALID_ITEM_ID if media_type == "favorites" else None, "url": "file:///var/lib/squeezeboxserver/music/track_3.mp3", }, diff --git a/tests/components/squeezebox/snapshots/test_media_player.ambr b/tests/components/squeezebox/snapshots/test_media_player.ambr index ddd5b9868a1..fd663c5eb63 100644 --- a/tests/components/squeezebox/snapshots/test_media_player.ambr +++ b/tests/components/squeezebox/snapshots/test_media_player.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -43,6 +44,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 0a03bcc291c..c5efe66152f 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -6,11 +6,11 @@ from unittest.mock import patch from pysqueezebox import Server from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.squeezebox.const import CONF_HTTPS, DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -143,27 +143,128 @@ async def test_user_form_duplicate(hass: HomeAssistant) -> None: async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "edit"} - ) async def patch_async_query(self, *args): self.http_status = HTTPStatus.UNAUTHORIZED return False - with patch("pysqueezebox.Server.async_query", new=patch_async_query): + with ( + patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ), + patch( + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_discover, + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + + with patch( + "homeassistant.components.squeezebox.config_flow.Server.async_query", + new=patch_async_query, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: HOST, CONF_PORT: PORT, - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, }, ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + } - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "invalid_auth"} + +async def test_form_validate_exception(hass: HomeAssistant) -> None: + """Test we handle exception.""" + + with ( + patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ), + patch( + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_discover, + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + + with patch( + "homeassistant.components.squeezebox.config_flow.Server.async_query", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + } async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -232,7 +333,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbccddeeff", hostname="any", @@ -254,7 +355,7 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbccddeeff", hostname="any", @@ -273,7 +374,7 @@ async def test_dhcp_discovery_existing_player(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.1.1.1", macaddress="aabbccddeeff", hostname="any", diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index b612bc9f3f3..b1d5b958d47 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -12,7 +12,7 @@ import pytest from homeassistant.components.srp_energy.const import DOMAIN, PHOENIX_TIME_ZONE from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import MOCK_USAGE, TEST_CONFIG_HOME diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 7dc0f0095d4..56623b51bb5 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -2,6 +2,7 @@ from datetime import datetime from ipaddress import IPv4Address +from typing import Any from unittest.mock import ANY, AsyncMock, patch from async_upnp_client.server import UpnpServer @@ -19,13 +20,32 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery_flow import DiscoveryKey +from homeassistant.helpers.service_info.ssdp import ( + ATTR_NT, + ATTR_ST, + ATTR_UPNP_DEVICE_TYPE, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MANUFACTURER_URL, + ATTR_UPNP_MODEL_DESCRIPTION, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_MODEL_NUMBER, + ATTR_UPNP_MODEL_URL, + ATTR_UPNP_PRESENTATION_URL, + ATTR_UPNP_SERIAL, + ATTR_UPNP_SERVICE_LIST, + ATTR_UPNP_UDN, + ATTR_UPNP_UPC, + SsdpServiceInfo, +) from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, MockModule, async_fire_time_changed, + import_and_test_deprecated_constant, mock_integration, ) from tests.test_util.aiohttp import AiohttpClientMocker @@ -74,7 +94,7 @@ async def test_ssdp_flow_dispatched_on_st( "discovery_key": DiscoveryKey(domain="ssdp", key="uuid:mock-udn", version=1), "source": config_entries.SOURCE_SSDP, } - mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] + mock_call_data: SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] assert mock_call_data.ssdp_st == "mock-st" assert mock_call_data.ssdp_location == "http://1.1.1.1" assert mock_call_data.ssdp_usn == "uuid:mock-udn::mock-st" @@ -83,7 +103,7 @@ async def test_ssdp_flow_dispatched_on_st( assert mock_call_data.ssdp_udn == ANY assert mock_call_data.ssdp_headers["_timestamp"] == ANY assert mock_call_data.x_homeassistant_matching_domains == {"mock-domain"} - assert mock_call_data.upnp == {ssdp.ATTR_UPNP_UDN: "uuid:mock-udn"} + assert mock_call_data.upnp == {ATTR_UPNP_UDN: "uuid:mock-udn"} assert "Failed to fetch ssdp data" not in caplog.text @@ -118,7 +138,7 @@ async def test_ssdp_flow_dispatched_on_manufacturer_url( "discovery_key": DiscoveryKey(domain="ssdp", key="uuid:mock-udn", version=1), "source": config_entries.SOURCE_SSDP, } - mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] + mock_call_data: SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] assert mock_call_data.ssdp_st == "mock-st" assert mock_call_data.ssdp_location == "http://1.1.1.1" assert mock_call_data.ssdp_usn == "uuid:mock-udn::mock-st" @@ -127,7 +147,7 @@ async def test_ssdp_flow_dispatched_on_manufacturer_url( assert mock_call_data.ssdp_udn == ANY assert mock_call_data.ssdp_headers["_timestamp"] == ANY assert mock_call_data.x_homeassistant_matching_domains == {"mock-domain"} - assert mock_call_data.upnp == {ssdp.ATTR_UPNP_UDN: "uuid:mock-udn"} + assert mock_call_data.upnp == {ATTR_UPNP_UDN: "uuid:mock-udn"} assert "Failed to fetch ssdp data" not in caplog.text @@ -227,8 +247,8 @@ async def test_scan_match_upnp_devicedesc_devicetype( return_value={ "mock-domain": [ { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_MANUFACTURER: "Paulus", } ] }, @@ -270,8 +290,8 @@ async def test_scan_not_all_present( return_value={ "mock-domain": [ { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_MANUFACTURER: "Not-Paulus", } ] }, @@ -455,8 +475,8 @@ async def test_discovery_from_advertisement_sets_ssdp_st( assert discovery_info.ssdp_headers["nts"] == "ssdp:alive" assert discovery_info.ssdp_headers["_timestamp"] == ANY assert discovery_info.upnp == { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_UDN: "uuid:mock-udn", } @@ -555,7 +575,7 @@ async def test_scan_with_registered_callback( 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] + mock_call_data: SsdpServiceInfo = async_integration_callback.call_args[0][0] assert mock_call_data.ssdp_ext == "" assert mock_call_data.ssdp_location == "http://1.1.1.1" assert mock_call_data.ssdp_server == "mock-server" @@ -568,8 +588,8 @@ async def test_scan_with_registered_callback( assert mock_call_data.ssdp_headers["_timestamp"] == ANY assert mock_call_data.x_homeassistant_matching_domains == set() assert mock_call_data.upnp == { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } assert "Exception in SSDP callback" in caplog.text @@ -627,8 +647,8 @@ async def test_getting_existing_headers( assert discovery_info_by_st.ssdp_udn == ANY assert discovery_info_by_st.ssdp_headers["_timestamp"] == ANY assert discovery_info_by_st.upnp == { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } discovery_info_by_udn = await ssdp.async_get_discovery_info_by_udn( @@ -646,8 +666,8 @@ async def test_getting_existing_headers( assert discovery_info_by_udn.ssdp_udn == ANY assert discovery_info_by_udn.ssdp_headers["_timestamp"] == ANY assert discovery_info_by_udn.upnp == { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } discovery_info_by_udn_st = await ssdp.async_get_discovery_info_by_udn_st( @@ -664,8 +684,8 @@ async def test_getting_existing_headers( assert discovery_info_by_udn_st.ssdp_udn == ANY assert discovery_info_by_udn_st.ssdp_headers["_timestamp"] == ANY assert discovery_info_by_udn_st.upnp == { - ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", - ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", + ATTR_UPNP_DEVICE_TYPE: "Paulus", + ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } assert ( @@ -713,7 +733,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ return_value={ "mock-domain": [ { - ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + ATTR_UPNP_DEVICE_TYPE: "ABC", } ] }, @@ -738,7 +758,7 @@ async def test_async_detect_interfaces_setting_empty_route( return_value={ "mock-domain": [ { - ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + ATTR_UPNP_DEVICE_TYPE: "ABC", } ] }, @@ -787,7 +807,7 @@ async def test_bind_failure_skips_adapter( return_value={ "mock-domain": [ { - ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + ATTR_UPNP_DEVICE_TYPE: "ABC", } ] }, @@ -999,7 +1019,7 @@ async def test_ssdp_rediscover( assert len(mock_flow_init.mock_calls) == 1 assert mock_flow_init.mock_calls[0][1][0] == "mock-domain" assert mock_flow_init.mock_calls[0][2]["context"] == expected_context - mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] + mock_call_data: SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] assert mock_call_data.ssdp_st == "mock-st" assert mock_call_data.ssdp_location == "http://1.1.1.1" @@ -1086,7 +1106,7 @@ async def test_ssdp_rediscover_no_match( assert len(mock_flow_init.mock_calls) == 1 assert mock_flow_init.mock_calls[0][1][0] == "mock-domain" assert mock_flow_init.mock_calls[0][2]["context"] == expected_context - mock_call_data: ssdp.SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] + mock_call_data: SsdpServiceInfo = mock_flow_init.mock_calls[0][2]["data"] assert mock_call_data.ssdp_st == "mock-st" assert mock_call_data.ssdp_location == "http://1.1.1.1" @@ -1094,3 +1114,105 @@ async def test_ssdp_rediscover_no_match( await hass.async_block_till_done() assert len(mock_flow_init.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("constant_name", "replacement_name", "replacement"), + [ + ( + "SsdpServiceInfo", + "homeassistant.helpers.service_info.ssdp.SsdpServiceInfo", + SsdpServiceInfo, + ), + ( + "ATTR_ST", + "homeassistant.helpers.service_info.ssdp.ATTR_ST", + ATTR_ST, + ), + ( + "ATTR_NT", + "homeassistant.helpers.service_info.ssdp.ATTR_NT", + ATTR_NT, + ), + ( + "ATTR_UPNP_DEVICE_TYPE", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_DEVICE_TYPE", + ATTR_UPNP_DEVICE_TYPE, + ), + ( + "ATTR_UPNP_FRIENDLY_NAME", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_FRIENDLY_NAME", + ATTR_UPNP_FRIENDLY_NAME, + ), + ( + "ATTR_UPNP_MANUFACTURER", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_MANUFACTURER", + ATTR_UPNP_MANUFACTURER, + ), + ( + "ATTR_UPNP_MANUFACTURER_URL", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_MANUFACTURER_URL", + ATTR_UPNP_MANUFACTURER_URL, + ), + ( + "ATTR_UPNP_MODEL_DESCRIPTION", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_MODEL_DESCRIPTION", + ATTR_UPNP_MODEL_DESCRIPTION, + ), + ( + "ATTR_UPNP_MODEL_NAME", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_MODEL_NAME", + ATTR_UPNP_MODEL_NAME, + ), + ( + "ATTR_UPNP_MODEL_NUMBER", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_MODEL_NUMBER", + ATTR_UPNP_MODEL_NUMBER, + ), + ( + "ATTR_UPNP_MODEL_URL", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_MODEL_URL", + ATTR_UPNP_MODEL_URL, + ), + ( + "ATTR_UPNP_SERIAL", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_SERIAL", + ATTR_UPNP_SERIAL, + ), + ( + "ATTR_UPNP_SERVICE_LIST", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_SERVICE_LIST", + ATTR_UPNP_SERVICE_LIST, + ), + ( + "ATTR_UPNP_UDN", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_UDN", + ATTR_UPNP_UDN, + ), + ( + "ATTR_UPNP_UPC", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_UPC", + ATTR_UPNP_UPC, + ), + ( + "ATTR_UPNP_PRESENTATION_URL", + "homeassistant.helpers.service_info.ssdp.ATTR_UPNP_PRESENTATION_URL", + ATTR_UPNP_PRESENTATION_URL, + ), + ], +) +def test_deprecated_constants( + caplog: pytest.LogCaptureFixture, + constant_name: str, + replacement_name: str, + replacement: Any, +) -> None: + """Test deprecated automation constants.""" + import_and_test_deprecated_constant( + caplog, + ssdp, + constant_name, + replacement_name, + replacement, + "2026.2", + ) diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 7e04c21562a..f15a80771cf 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -33,8 +33,9 @@ async def test_successful_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.runtime_data + assert entry.runtime_data.data assert entry.state is ConfigEntryState.LOADED - assert entry.entry_id in hass.data[DOMAIN] async def test_unload_entry(hass: HomeAssistant) -> None: @@ -59,4 +60,3 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert entry.state is ConfigEntryState.NOT_LOADED - assert entry.entry_id not in hass.data[DOMAIN] diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index 40578113bb3..5e963f77a2b 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.steamist.const import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import ( DEFAULT_ENTRY_DATA, @@ -30,7 +30,7 @@ from tests.common import MockConfigEntry MODULE = "homeassistant.components.steamist" -DHCP_DISCOVERY = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY = DhcpServiceInfo( hostname=DEVICE_HOSTNAME, ip=DEVICE_IP_ADDRESS, macaddress=DEVICE_MAC_ADDRESS.lower().replace(":", ""), @@ -238,7 +238,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="any", ip=DEVICE_IP_ADDRESS, macaddress="000000000000", diff --git a/tests/components/steamist/test_switch.py b/tests/components/steamist/test_switch.py index a20bebc4052..cd62c18590a 100644 --- a/tests/components/steamist/test_switch.py +++ b/tests/components/steamist/test_switch.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import ( MOCK_ASYNC_GET_STATUS_ACTIVE, diff --git a/tests/components/stookwijzer/snapshots/test_sensor.ambr b/tests/components/stookwijzer/snapshots/test_sensor.ambr index f6751a84f22..ff1f6a12b8a 100644 --- a/tests/components/stookwijzer/snapshots/test_sensor.ambr +++ b/tests/components/stookwijzer/snapshots/test_sensor.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -67,6 +68,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -118,6 +120,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/stookwijzer/test_init.py b/tests/components/stookwijzer/test_init.py index 0df9b55d1a9..ddefb6be772 100644 --- a/tests/components/stookwijzer/test_init.py +++ b/tests/components/stookwijzer/test_init.py @@ -100,6 +100,7 @@ async def test_entity_entry_migration( entity_registry: er.EntityRegistry, ) -> None: """Test successful migration of entry data.""" + mock_config_entry.add_to_hass(hass) entity = entity_registry.async_get_or_create( suggested_object_id="advice", disabled_by=None, diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index babd7c0b748..c96b7d9427f 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -19,7 +19,7 @@ from homeassistant.components.stream.const import ( from homeassistant.components.stream.core import Orientation, Part from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( FAKE_TIME, @@ -119,13 +119,16 @@ def make_playlist( response.extend( [ f"#EXT-X-PART-INF:PART-TARGET={part_target_duration:.3f}", - f"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={2*part_target_duration:.3f}", - f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START_LL_HLS*part_target_duration:.3f},PRECISE=YES", + "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=" + f"{2 * part_target_duration:.3f}", + "#EXT-X-START:TIME-OFFSET=-" + f"{EXT_X_START_LL_HLS * part_target_duration:.3f},PRECISE=YES", ] ) else: response.append( - f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START_NON_LL_HLS*segment_duration:.3f},PRECISE=YES", + "#EXT-X-START:TIME-OFFSET=-" + f"{EXT_X_START_NON_LL_HLS * segment_duration:.3f},PRECISE=YES", ) if segments: response.extend(segments) @@ -278,8 +281,19 @@ async def test_stream_timeout_after_stop( await hass.async_block_till_done() +@pytest.mark.parametrize( + ("exception"), + [ + # pylint: disable-next=c-extension-no-member + (av.error.InvalidDataError(-2, "error")), + (av.HTTPBadRequestError(500, "error")), + ], +) async def test_stream_retries( - hass: HomeAssistant, setup_component, should_retry + hass: HomeAssistant, + setup_component, + should_retry, + exception, ) -> None: """Test hls stream is retried on failure.""" # Setup demo HLS track @@ -309,8 +323,7 @@ async def test_stream_retries( def av_open_side_effect(*args, **kwargs): hass.loop.call_soon_threadsafe(futures.pop().set_result, None) - # pylint: disable-next=c-extension-no-member - raise av.error.InvalidDataError(-2, "error") + raise exception with ( patch("av.open") as av_open, diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 1ae6f9e8931..ba5f8d2e70f 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,15 +1,41 @@ """Test stream init.""" import logging +from typing import Any +from unittest.mock import MagicMock, patch import av import pytest -from homeassistant.components.stream import __name__ as stream_name +from homeassistant.components.stream import ( + SOURCE_TIMEOUT, + StreamClientError, + StreamOpenClientError, + __name__ as stream_name, + async_check_stream_client_error, + create_stream, +) +from homeassistant.components.stream.const import ATTR_PREFER_TCP from homeassistant.const import EVENT_LOGGING_CHANGED from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from .common import dynamic_stream_settings + + +async def test_stream_not_setup(hass: HomeAssistant, h264_video) -> None: + """Test hls stream. + + Purposefully not mocking anything here to test full + integration with the stream component. + """ + with pytest.raises(HomeAssistantError, match="Stream integration is not set up"): + create_stream(hass, "rtsp://foobar", {}, dynamic_stream_settings()) + + with pytest.raises(HomeAssistantError, match="Stream integration is not set up"): + await async_check_stream_client_error(hass, "rtsp://foobar") + async def test_log_levels( hass: HomeAssistant, caplog: pytest.LogCaptureFixture @@ -53,3 +79,111 @@ async def test_log_levels( assert "SHOULD PASS" in caplog.text assert "SHOULD NOT PASS" not in caplog.text + + +async def test_check_open_stream_params(hass: HomeAssistant) -> None: + """Test check open stream params.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + container_mock = MagicMock() + source = "rtsp://foobar" + + with patch("av.open", return_value=container_mock) as open_mock: + await async_check_stream_client_error(hass, source) + + options = { + "rtsp_flags": ATTR_PREFER_TCP, + "stimeout": "5000000", + } + open_mock.assert_called_once_with(source, options=options, timeout=SOURCE_TIMEOUT) + container_mock.close.assert_called_once() + + container_mock.reset_mock() + with ( + patch("av.open", return_value=container_mock) as open_mock, + pytest.raises(HomeAssistantError, match="Invalid stream options"), + ): + await async_check_stream_client_error(hass, source, {"foo": "bar"}) + + +@pytest.mark.parametrize( + ("error", "enum_result"), + [ + pytest.param( + av.HTTPBadRequestError(400, ""), + StreamClientError.BadRequest, + id="BadRequest", + ), + pytest.param( + av.HTTPUnauthorizedError(401, ""), + StreamClientError.Unauthorized, + id="Unauthorized", + ), + pytest.param( + av.HTTPForbiddenError(403, ""), StreamClientError.Forbidden, id="Forbidden" + ), + pytest.param( + av.HTTPNotFoundError(404, ""), StreamClientError.NotFound, id="NotFound" + ), + pytest.param( + av.HTTPOtherClientError(408, ""), StreamClientError.Other, id="Other" + ), + ], +) +async def test_try_open_stream_error( + hass: HomeAssistant, error: av.HTTPClientError, enum_result: StreamClientError +) -> None: + """Test trying to open a stream.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + with ( + patch("av.open", side_effect=error), + pytest.raises(StreamOpenClientError) as ex, + ): + await async_check_stream_client_error(hass, "rtsp://foobar") + assert ex.value.error_code is enum_result + + +@pytest.mark.parametrize( + ("options", "expected_pyav_options"), + [ + ( + {}, + {"rtsp_flags": "prefer_tcp", "stimeout": "5000000"}, + ), + ( + {"rtsp_transport": "udp"}, + { + "rtsp_flags": "prefer_tcp", + "rtsp_transport": "udp", + "stimeout": "5000000", + }, + ), + ( + {"use_wallclock_as_timestamps": True}, + { + "rtsp_flags": "prefer_tcp", + "stimeout": "5000000", + "use_wallclock_as_timestamps": "1", + }, + ), + ], +) +async def test_convert_stream_options( + hass: HomeAssistant, + options: dict[str, Any], + expected_pyav_options: dict[str, Any], +) -> None: + """Test stream options.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + container_mock = MagicMock() + source = "rtsp://foobar" + + with patch("av.open", return_value=container_mock) as open_mock: + await async_check_stream_client_error(hass, source, options) + + open_mock.assert_called_once_with( + source, options=expected_pyav_options, timeout=SOURCE_TIMEOUT + ) + container_mock.close.assert_called_once() diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 5577076830b..443103fdf92 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -99,18 +99,17 @@ def make_segment_with_parts( if discontinuity: response.append("#EXT-X-DISCONTINUITY") response.extend( - f'#EXT-X-PART:DURATION={TEST_PART_DURATION:.3f},URI="./segment/{segment}.{i}.m4s"{",INDEPENDENT=YES" if i%independent_period==0 else ""}' + f"#EXT-X-PART:DURATION={TEST_PART_DURATION:.3f}," + f'URI="./segment/{segment}.{i}.m4s"' + f"{',INDEPENDENT=YES' if i % independent_period == 0 else ''}" for i in range(num_parts) ) - response.extend( - [ - "#EXT-X-PROGRAM-DATE-TIME:" - + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] - + "Z", - f"#EXTINF:{math.ceil(SEGMENT_DURATION/TEST_PART_DURATION)*TEST_PART_DURATION:.3f},", - f"./segment/{segment}.m4s", - ] + response.append( + f"#EXT-X-PROGRAM-DATE-TIME:{FAKE_TIME.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]}Z" ) + duration = math.ceil(SEGMENT_DURATION / TEST_PART_DURATION) * TEST_PART_DURATION + response.append(f"#EXTINF:{duration:.3f},") + response.append(f"./segment/{segment}.m4s") return "\n".join(response) @@ -166,7 +165,7 @@ async def test_ll_hls_stream( # Fetch playlist playlist_url = "/" + master_playlist.splitlines()[-1] playlist_response = await hls_client.get( - playlist_url + f"?_HLS_msn={num_playlist_segments-1}" + playlist_url + f"?_HLS_msn={num_playlist_segments - 1}" ) assert playlist_response.status == HTTPStatus.OK @@ -465,7 +464,8 @@ async def test_ll_hls_playlist_bad_msn_part( ).status == HTTPStatus.BAD_REQUEST assert ( await hls_client.get( - f"/playlist.m3u8?_HLS_msn=1&_HLS_part={num_completed_parts-1+hass.data[DOMAIN][ATTR_SETTINGS].hls_advance_part_limit}" + "/playlist.m3u8?_HLS_msn=1&_HLS_part=" + f"{num_completed_parts - 1 + hass.data[DOMAIN][ATTR_SETTINGS].hls_advance_part_limit}" ) ).status == HTTPStatus.BAD_REQUEST stream_worker_sync.resume() @@ -515,13 +515,13 @@ async def test_ll_hls_playlist_rollover_part( *( [ hls_client.get( - f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts)-1}" + f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts) - 1}" ), hls_client.get( f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts)}" ), hls_client.get( - f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts)+1}" + f"/playlist.m3u8?_HLS_msn=1&_HLS_part={len(segment.parts) + 1}" ), hls_client.get("/playlist.m3u8?_HLS_msn=2&_HLS_part=0"), ] diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 8e079cded45..7c856180f77 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -21,7 +21,7 @@ from homeassistant.components.stream.fmp4utils import find_box from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( DefaultSegment as Segment, diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 73c51087ef1..2be972cc6a2 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -41,6 +41,7 @@ from homeassistant.components.stream.const import ( TARGET_SEGMENT_DURATION_NON_LL_HLS, ) from homeassistant.components.stream.core import Orientation, StreamSettings +from homeassistant.components.stream.exceptions import StreamClientError from homeassistant.components.stream.worker import ( StreamEndedError, StreamState, @@ -341,7 +342,18 @@ async def async_decode_stream( return py_av.capture_buffer -async def test_stream_open_fails(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("exception", "error_code"), + [ + # pylint: disable-next=c-extension-no-member + (av.error.InvalidDataError(-2, "error"), StreamClientError.Other), + (av.HTTPBadRequestError(400, ""), StreamClientError.BadRequest), + (av.HTTPUnauthorizedError(401, ""), StreamClientError.Unauthorized), + ], +) +async def test_stream_open_fails( + hass: HomeAssistant, exception: Exception, error_code: StreamClientError +) -> None: """Test failure on stream open.""" stream = Stream( hass, @@ -352,12 +364,11 @@ async def test_stream_open_fails(hass: HomeAssistant) -> None: ) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open: - # pylint: disable-next=c-extension-no-member - av_open.side_effect = av.error.InvalidDataError(-2, "error") - with pytest.raises(StreamWorkerError): + av_open.side_effect = exception + with pytest.raises(StreamWorkerError) as err: run_worker(hass, stream, STREAM_SOURCE) - await hass.async_block_till_done() av_open.assert_called_once() + assert err.value.error_code == error_code async def test_stream_worker_success(hass: HomeAssistant) -> None: diff --git a/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr b/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr index c74df76e71b..d13a19bc656 100644 --- a/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr +++ b/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/streamlabswater/snapshots/test_sensor.ambr b/tests/components/streamlabswater/snapshots/test_sensor.ambr index d54cdcafb93..c1248f2c0a0 100644 --- a/tests/components/streamlabswater/snapshots/test_sensor.ambr +++ b/tests/components/streamlabswater/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -57,6 +58,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -108,6 +110,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 3d5daab2bec..e36ece52f57 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -16,7 +16,7 @@ from homeassistant.components.stt import ( ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.setup import async_setup_component from .common import ( @@ -145,7 +145,7 @@ async def mock_config_entry_setup( async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test stt platform via config entry.""" async_add_entities([mock_provider_entity]) diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index e18ea8fd398..84d2fde97f7 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .api_responses import TEST_VIN_2_EV, VEHICLE_DATA, VEHICLE_STATUS_EV diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index f634a053c65..73557fd3bde 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -1,20 +1,21 @@ """Common fixtures for the Suez Water tests.""" from collections.abc import Generator +from datetime import date from unittest.mock import AsyncMock, patch from pysuez import AggregatedData, PriceResult from pysuez.const import ATTRIBUTION import pytest -from homeassistant.components.suez_water.const import DOMAIN +from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN from tests.common import MockConfigEntry MOCK_DATA = { "username": "test-username", "password": "test-password", - "counter_id": "test-counter", + CONF_COUNTER_ID: "test-counter", } @@ -22,10 +23,11 @@ MOCK_DATA = { def mock_config_entry() -> MockConfigEntry: """Create mock config_entry needed by suez_water integration.""" return MockConfigEntry( - unique_id=MOCK_DATA["username"], + unique_id=MOCK_DATA[CONF_COUNTER_ID], domain=DOMAIN, title="Suez mock device", data=MOCK_DATA, + version=2, ) @@ -56,22 +58,22 @@ def mock_suez_client() -> Generator[AsyncMock]: result = AggregatedData( value=160, current_month={ - "2024-01-01": 130, - "2024-01-02": 145, + date.fromisoformat("2024-01-01"): 130, + date.fromisoformat("2024-01-02"): 145, }, previous_month={ - "2024-12-01": 154, - "2024-12-02": 166, + date.fromisoformat("2024-12-01"): 154, + date.fromisoformat("2024-12-02"): 166, }, current_year=1500, previous_year=1000, attribution=ATTRIBUTION, highest_monthly_consumption=2558, history={ - "2024-01-01": 130, - "2024-01-02": 145, - "2024-12-01": 154, - "2024-12-02": 166, + date.fromisoformat("2024-01-01"): 130, + date.fromisoformat("2024-01-02"): 145, + date.fromisoformat("2024-12-01"): 154, + date.fromisoformat("2024-12-02"): 166, }, ) diff --git a/tests/components/suez_water/snapshots/test_sensor.ambr b/tests/components/suez_water/snapshots/test_sensor.ambr index da0ed3df7dd..536e79df606 100644 --- a/tests/components/suez_water/snapshots/test_sensor.ambr +++ b/tests/components/suez_water/snapshots/test_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,6 +56,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index 6779b4c7d02..bebb4fd72ac 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -32,8 +32,8 @@ async def test_form( await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-username" - assert result["result"].unique_id == "test-username" + assert result["title"] == MOCK_DATA[CONF_COUNTER_ID] + assert result["result"].unique_id == MOCK_DATA[CONF_COUNTER_ID] assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -63,18 +63,20 @@ async def test_form_invalid_auth( await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-username" - assert result["result"].unique_id == "test-username" + assert result["title"] == MOCK_DATA[CONF_COUNTER_ID] + assert result["result"].unique_id == MOCK_DATA[CONF_COUNTER_ID] assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_already_configured(hass: HomeAssistant) -> None: +async def test_form_already_configured( + hass: HomeAssistant, suez_client: AsyncMock +) -> None: """Test we abort when entry is already configured.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="test-username", + unique_id=MOCK_DATA[CONF_COUNTER_ID], data=MOCK_DATA, ) entry.add_to_hass(hass) @@ -124,7 +126,7 @@ async def test_form_error( ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-username" + assert result["title"] == MOCK_DATA[CONF_COUNTER_ID] assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -139,7 +141,7 @@ async def test_form_auto_counter( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - partial_form = {**MOCK_DATA} + partial_form = MOCK_DATA.copy() partial_form.pop(CONF_COUNTER_ID) suez_client.find_counter.side_effect = PySuezError("test counter not found") @@ -160,7 +162,7 @@ async def test_form_auto_counter( await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "test-username" - assert result["result"].unique_id == "test-username" + assert result["title"] == MOCK_DATA[CONF_COUNTER_ID] + assert result["result"].unique_id == MOCK_DATA[CONF_COUNTER_ID] assert result["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/suez_water/test_init.py b/tests/components/suez_water/test_init.py index 78d086af38f..16d32b61dee 100644 --- a/tests/components/suez_water/test_init.py +++ b/tests/components/suez_water/test_init.py @@ -2,11 +2,14 @@ from unittest.mock import AsyncMock +from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN from homeassistant.components.suez_water.coordinator import PySuezError from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from . import setup_integration +from .conftest import MOCK_DATA from tests.common import MockConfigEntry @@ -35,3 +38,56 @@ async def test_initialization_setup_api_error( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_migration_version_rollback( + hass: HomeAssistant, + suez_client: AsyncMock, +) -> None: + """Test that downgrading from a future version is not possible.""" + future_entry = MockConfigEntry( + unique_id=MOCK_DATA[CONF_COUNTER_ID], + domain=DOMAIN, + title="Suez mock device", + data=MOCK_DATA, + version=3, + ) + await setup_integration(hass, future_entry) + assert future_entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_no_migration_current_version( + hass: HomeAssistant, + suez_client: AsyncMock, +) -> None: + """Test that a current version does not migrate.""" + current_entry = MockConfigEntry( + unique_id=MOCK_DATA[CONF_COUNTER_ID], + domain=DOMAIN, + title="Suez mock device", + data=MOCK_DATA, + version=2, + ) + await setup_integration(hass, current_entry) + assert current_entry.state is ConfigEntryState.LOADED + assert current_entry.unique_id == MOCK_DATA[CONF_COUNTER_ID] + + +async def test_migration_version_1_to_2( + hass: HomeAssistant, + suez_client: AsyncMock, +) -> None: + """Test that a migration from 1 to 2 changes the unique_id.""" + past_entry = MockConfigEntry( + unique_id=MOCK_DATA[CONF_USERNAME], + domain=DOMAIN, + title=MOCK_DATA[CONF_USERNAME], + data=MOCK_DATA, + version=1, + ) + + await setup_integration(hass, past_entry) + assert past_entry.state is ConfigEntryState.LOADED + assert past_entry.unique_id == MOCK_DATA[CONF_COUNTER_ID] + assert past_entry.title == MOCK_DATA[CONF_USERNAME] + assert past_entry.version == 2 diff --git a/tests/components/suez_water/test_sensor.py b/tests/components/suez_water/test_sensor.py index cb578432f62..950d5d8393d 100644 --- a/tests/components/suez_water/test_sensor.py +++ b/tests/components/suez_water/test_sensor.py @@ -1,5 +1,6 @@ """Test Suez_water sensor platform.""" +from datetime import date from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory @@ -32,6 +33,13 @@ async def test_sensors_valid_state( assert mock_config_entry.state is ConfigEntryState.LOADED await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + state = hass.states.get("sensor.suez_mock_device_water_usage_yesterday") + assert state + previous: dict = state.attributes["previous_month_consumption"] + assert previous + assert previous.get(date.fromisoformat("2024-12-01")) is None + assert previous.get(str(date.fromisoformat("2024-12-01"))) == 154 + @pytest.mark.parametrize("method", [("fetch_aggregated_data"), ("get_price")]) async def test_sensors_failed_update( diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index a30076d6d3c..3896498bbb0 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.sun import entity from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/sun/test_sensor.py b/tests/components/sun/test_sensor.py index cb97ae565c7..59e4e4c700b 100644 --- a/tests/components/sun/test_sensor.py +++ b/tests/components/sun/test_sensor.py @@ -10,9 +10,9 @@ import pytest from homeassistant.components import sun from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 303ca3b80cd..a7aeae25ac7 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, mock_component diff --git a/tests/components/swiss_public_transport/conftest.py b/tests/components/swiss_public_transport/conftest.py index 88bd233765b..03924ba4e2f 100644 --- a/tests/components/swiss_public_transport/conftest.py +++ b/tests/components/swiss_public_transport/conftest.py @@ -1,10 +1,57 @@ """Common fixtures for the swiss_public_transport tests.""" from collections.abc import Generator +import json from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.swiss_public_transport.const import ( + CONF_DESTINATION, + CONF_START, + DOMAIN, +) + +from tests.common import MockConfigEntry, load_fixture + +START = "Zürich" +DESTINATION = "Bern" + + +@pytest.fixture +def mock_opendata_client() -> Generator[AsyncMock]: + """Mock a Opendata client.""" + with ( + patch( + "homeassistant.components.swiss_public_transport.OpendataTransport", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.swiss_public_transport.config_flow.OpendataTransport", + new=mock_client, + ), + ): + client = mock_client.return_value + client.async_get_data.return_value = None + client.from_name = START + client.to_name = DESTINATION + client.connections = json.loads(load_fixture("connections.json", DOMAIN))[0:3] + yield client + + +@pytest.fixture(name="swiss_public_transport_config_entry") +def mock_swiss_public_transport_config_entry() -> MockConfigEntry: + """Mock cookidoo configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_START: START, + CONF_DESTINATION: DESTINATION, + }, + title=f"{START} {DESTINATION}", + entry_id="01JBVVVJ87F6G5V0QJX6HBC94T", + ) + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: diff --git a/tests/components/swiss_public_transport/fixtures/connections.json b/tests/components/swiss_public_transport/fixtures/connections.json index 7e61206c366..1e8e5022bdf 100644 --- a/tests/components/swiss_public_transport/fixtures/connections.json +++ b/tests/components/swiss_public_transport/fixtures/connections.json @@ -23,8 +23,7 @@ "platform": 2, "transfers": 0, "duration": "10", - "delay": 0, - "line": "T10" + "delay": 0 }, { "departure": "2024-01-06T18:06:00+0100", diff --git a/tests/components/swiss_public_transport/snapshots/test_sensor.ambr b/tests/components/swiss_public_transport/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..5ba65b2bd70 --- /dev/null +++ b/tests/components/swiss_public_transport/snapshots/test_sensor.ambr @@ -0,0 +1,395 @@ +# serializer version: 1 +# name: test_all_entities[sensor.zurich_bern_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_delay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Delay', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'delay', + 'unique_id': 'Zürich Bern_delay', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.zurich_bern_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'device_class': 'duration', + 'friendly_name': 'Zürich Bern Delay', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_departure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_departure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Departure', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'departure0', + 'unique_id': 'Zürich Bern_departure', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.zurich_bern_departure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'device_class': 'timestamp', + 'friendly_name': 'Zürich Bern Departure', + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_departure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-06T17:03:00+00:00', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_departure_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_departure_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Departure +1', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'departure1', + 'unique_id': 'Zürich Bern_departure1', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.zurich_bern_departure_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'device_class': 'timestamp', + 'friendly_name': 'Zürich Bern Departure +1', + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_departure_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-06T17:04:00+00:00', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_departure_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_departure_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Departure +2', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'departure2', + 'unique_id': 'Zürich Bern_departure2', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.zurich_bern_departure_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'device_class': 'timestamp', + 'friendly_name': 'Zürich Bern Departure +2', + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_departure_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-06T17:05:00+00:00', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_line-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_line', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Line', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'line', + 'unique_id': 'Zürich Bern_line', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.zurich_bern_line-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'friendly_name': 'Zürich Bern Line', + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_line', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'T10', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_platform-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_platform', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Platform', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'platform', + 'unique_id': 'Zürich Bern_platform', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.zurich_bern_platform-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'friendly_name': 'Zürich Bern Platform', + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_platform', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_transfers-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_transfers', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Transfers', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'transfers', + 'unique_id': 'Zürich Bern_transfers', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.zurich_bern_transfers-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'friendly_name': 'Zürich Bern Transfers', + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_transfers', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.zurich_bern_trip_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.zurich_bern_trip_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Trip duration', + 'platform': 'swiss_public_transport', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'trip_duration', + 'unique_id': 'Zürich Bern_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.zurich_bern_trip_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by transport.opendata.ch', + 'device_class': 'duration', + 'friendly_name': 'Zürich Bern Trip duration', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.zurich_bern_trip_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.003', + }) +# --- diff --git a/tests/components/swiss_public_transport/test_sensor.py b/tests/components/swiss_public_transport/test_sensor.py new file mode 100644 index 00000000000..6e832728277 --- /dev/null +++ b/tests/components/swiss_public_transport/test_sensor.py @@ -0,0 +1,145 @@ +"""Tests for the swiss_public_transport sensor platform.""" + +import json +from unittest.mock import AsyncMock, patch + +from opendata_transport.exceptions import ( + OpendataTransportConnectionError, + OpendataTransportError, +) +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.swiss_public_transport.const import ( + DEFAULT_UPDATE_TIME, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + snapshot_platform, +) +from tests.test_config_entries import FrozenDateTimeFactory + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_opendata_client: AsyncMock, + swiss_public_transport_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.cookidoo.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, swiss_public_transport_config_entry) + + assert swiss_public_transport_config_entry.state is ConfigEntryState.LOADED + + await snapshot_platform( + hass, entity_registry, snapshot, swiss_public_transport_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("raise_error"), + [OpendataTransportConnectionError, OpendataTransportError], +) +async def test_fetching_data( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_opendata_client: AsyncMock, + swiss_public_transport_config_entry: MockConfigEntry, + raise_error: Exception, +) -> None: + """Test fetching data.""" + await setup_integration(hass, swiss_public_transport_config_entry) + + assert swiss_public_transport_config_entry.state is ConfigEntryState.LOADED + + mock_opendata_client.async_get_data.assert_called() + + assert mock_opendata_client.async_get_data.call_count == 2 + + assert len(hass.states.async_all(SENSOR_DOMAIN)) == 8 + + assert ( + hass.states.get("sensor.zurich_bern_departure").state + == "2024-01-06T17:03:00+00:00" + ) + assert ( + hass.states.get("sensor.zurich_bern_departure_1").state + == "2024-01-06T17:04:00+00:00" + ) + assert ( + hass.states.get("sensor.zurich_bern_departure_2").state + == "2024-01-06T17:05:00+00:00" + ) + assert hass.states.get("sensor.zurich_bern_trip_duration").state == "0.003" + assert hass.states.get("sensor.zurich_bern_platform").state == "0" + assert hass.states.get("sensor.zurich_bern_transfers").state == "0" + assert hass.states.get("sensor.zurich_bern_delay").state == "0" + assert hass.states.get("sensor.zurich_bern_line").state == "T10" + + # Set new data and verify it + mock_opendata_client.connections = json.loads( + load_fixture("connections.json", DOMAIN) + )[3:6] + freezer.tick(DEFAULT_UPDATE_TIME) + async_fire_time_changed(hass) + assert mock_opendata_client.async_get_data.call_count == 3 + assert ( + hass.states.get("sensor.zurich_bern_departure").state + == "2024-01-06T17:06:00+00:00" + ) + + # Simulate fetch exception + mock_opendata_client.async_get_data.side_effect = raise_error + freezer.tick(DEFAULT_UPDATE_TIME) + async_fire_time_changed(hass) + assert mock_opendata_client.async_get_data.call_count == 4 + assert hass.states.get("sensor.zurich_bern_departure").state == "unavailable" + + # Recover and fetch new data again + mock_opendata_client.async_get_data.side_effect = None + mock_opendata_client.connections = json.loads( + load_fixture("connections.json", DOMAIN) + )[6:9] + freezer.tick(DEFAULT_UPDATE_TIME) + async_fire_time_changed(hass) + assert mock_opendata_client.async_get_data.call_count == 5 + assert ( + hass.states.get("sensor.zurich_bern_departure").state + == "2024-01-06T17:09:00+00:00" + ) + + +@pytest.mark.parametrize( + ("raise_error", "state"), + [ + (OpendataTransportConnectionError, ConfigEntryState.SETUP_RETRY), + (OpendataTransportError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_fetching_data_setup_exception( + hass: HomeAssistant, + mock_opendata_client: AsyncMock, + swiss_public_transport_config_entry: MockConfigEntry, + raise_error: Exception, + state: ConfigEntryState, +) -> None: + """Test fetching data with setup exception.""" + + mock_opendata_client.async_get_data.side_effect = raise_error + + await setup_integration(hass, swiss_public_transport_config_entry) + + assert swiss_public_transport_config_entry.state is state diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 7c4f434b0a4..5c5737804e1 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 08e6ab6d0f6..81f8a93611d 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index bd3985ff062..4d6794b962f 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -230,3 +230,67 @@ WOMETERTHPC_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=True, tx_power=-127, ) + +WORELAY_SWITCH_1PM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="W1080000", + manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"<\x00\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="W1080000", + manufacturer_data={2409: b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"<\x00\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "W1080000"), + time=0, + connectable=True, + tx_power=-127, +) + +LEAK_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Any", + manufacturer_data={ + 2409: b"\xd6407D1\x02V\x90\x00\x00\x00\x00\x1e\x05\x00\x00\x00\x00" + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\\x00V"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Any", + manufacturer_data={ + 2409: b"\xd6407D1\x02V\x90\x00\x00\x00\x00\x1e\x05\x00\x00\x00\x00" + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\\x00V"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Any"), + time=0, + connectable=False, + tx_power=-127, +) + +REMOTE_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Any", + manufacturer_data={89: b"\xaa\xbb\xcc\xdd\xee\xff"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"b V\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Any", + manufacturer_data={89: b"\xaa\xbb\xcc\xdd\xee\xff"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"b V\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Any"), + time=0, + connectable=False, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index b0fba2a5f18..1038bd318f5 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.switchbot.const import ( CONF_LOCK_NIGHTLATCH, CONF_RETRY_COUNT, ) -from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_IGNORE, SOURCE_USER from homeassistant.const import ( CONF_ADDRESS, CONF_NAME, @@ -30,6 +30,7 @@ from . import ( WOHAND_SERVICE_INFO, WOHAND_SERVICE_INFO_NOT_CONNECTABLE, WOLOCK_SERVICE_INFO, + WORELAY_SWITCH_1PM_SERVICE_INFO, WOSENSORTH_SERVICE_INFO, init_integration, patch_async_setup_entry, @@ -95,7 +96,7 @@ async def test_bluetooth_discovery_requires_password(hass: HomeAssistant) -> Non assert len(mock_setup_entry.mock_calls) == 1 -async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: +async def test_bluetooth_discovery_encrypted_key(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a lock.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -103,18 +104,18 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: data=WOLOCK_SERVICE_INFO, ) assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "lock_choose_method" + assert result["step_id"] == "encrypted_choose_method" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"next_step_id": "lock_key"} + result["flow_id"], user_input={"next_step_id": "encrypted_key"} ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_key" + assert result["step_id"] == "encrypted_key" assert result["errors"] == {} with patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + "switchbot.SwitchbotLock.verify_encryption_key", return_value=False, ): result = await hass.config_entries.flow.async_configure( @@ -127,13 +128,13 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_key" + assert result["step_id"] == "encrypted_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", + "switchbot.SwitchbotLock.verify_encryption_key", return_value=True, ), ): @@ -158,6 +159,51 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_key(hass: HomeAssistant) -> None: + """Test discovery via bluetooth with a encrypted device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WORELAY_SWITCH_1PM_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_key"} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_key" + assert result["errors"] == {} + + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "switchbot.SwitchbotRelaySwitch.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"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Relay Switch 1PM EEFF" + assert result["data"] == { + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "relay_switch_1pm", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device when already setup.""" entry = MockConfigEntry( @@ -257,6 +303,39 @@ async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None assert result["reason"] == "no_devices_found" +async def test_user_setup_wohand_replaces_ignored(hass: HomeAssistant) -> None: + """Test setting up a switchbot replaces an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id="aabbccddeeff", source=SOURCE_IGNORE + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Bot EEFF" + assert result["data"] == { + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_user_setup_wocurtain(hass: HomeAssistant) -> None: """Test the user initiated form with password and valid mac.""" @@ -400,7 +479,7 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: +async def test_user_setup_woencrypted_key(hass: HomeAssistant) -> None: """Test the user initiated form for a lock.""" with patch( @@ -411,18 +490,18 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "lock_choose_method" + assert result["step_id"] == "encrypted_choose_method" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"next_step_id": "lock_key"} + result["flow_id"], user_input={"next_step_id": "encrypted_key"} ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_key" + assert result["step_id"] == "encrypted_key" assert result["errors"] == {} with patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + "switchbot.SwitchbotLock.verify_encryption_key", return_value=False, ): result = await hass.config_entries.flow.async_configure( @@ -435,13 +514,13 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_key" + assert result["step_id"] == "encrypted_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", + "switchbot.SwitchbotLock.verify_encryption_key", return_value=True, ), ): @@ -466,7 +545,7 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: +async def test_user_setup_woencrypted_auth(hass: HomeAssistant) -> None: """Test the user initiated form for a lock.""" with patch( @@ -477,18 +556,18 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "lock_choose_method" + assert result["step_id"] == "encrypted_choose_method" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"next_step_id": "lock_auth"} + result["flow_id"], user_input={"next_step_id": "encrypted_auth"} ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_auth" + assert result["step_id"] == "encrypted_auth" assert result["errors"] == {} with patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key", + "switchbot.SwitchbotLock.async_retrieve_encryption_key", side_effect=SwitchbotAuthenticationError("error from api"), ): result = await hass.config_entries.flow.async_configure( @@ -500,18 +579,18 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_auth" + assert result["step_id"] == "encrypted_auth" assert result["errors"] == {"base": "auth_failed"} assert "error from api" in result["description_placeholders"]["error_detail"] with ( patch_async_setup_entry() as mock_setup_entry, patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + "switchbot.SwitchbotLock.verify_encryption_key", return_value=True, ), patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key", + "switchbot.SwitchbotLock.async_retrieve_encryption_key", return_value={ CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", @@ -539,7 +618,9 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> None: +async def test_user_setup_woencrypted_auth_switchbot_api_down( + hass: HomeAssistant, +) -> None: """Test the user initiated form for a lock when the switchbot api is down.""" with patch( @@ -550,18 +631,18 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "lock_choose_method" + assert result["step_id"] == "encrypted_choose_method" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"next_step_id": "lock_auth"} + result["flow_id"], user_input={"next_step_id": "encrypted_auth"} ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_auth" + assert result["step_id"] == "encrypted_auth" assert result["errors"] == {} with patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key", + "switchbot.SwitchbotLock.async_retrieve_encryption_key", side_effect=SwitchbotAccountConnectionError("Switchbot API down"), ): result = await hass.config_entries.flow.async_configure( @@ -600,20 +681,20 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result["type"] is FlowResultType.MENU - assert result["step_id"] == "lock_choose_method" + assert result["step_id"] == "encrypted_choose_method" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"next_step_id": "lock_key"} + result["flow_id"], user_input={"next_step_id": "encrypted_key"} ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "lock_key" + assert result["step_id"] == "encrypted_key" assert result["errors"] == {} with ( patch_async_setup_entry() as mock_setup_entry, patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + "switchbot.SwitchbotLock.verify_encryption_key", return_value=True, ), ): @@ -845,3 +926,163 @@ async def test_options_flow_lock_pro(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 0 assert entry.options[CONF_LOCK_NIGHTLATCH] is True + + +async def test_user_setup_worelay_switch_1pm_key(hass: HomeAssistant) -> None: + """Test the user initiated form for a relay switch 1pm.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_key"} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_key" + assert result["errors"] == {} + + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "switchbot.SwitchbotRelaySwitch.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"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Relay Switch 1PM EEFF" + assert result["data"] == { + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "relay_switch_1pm", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_worelay_switch_1pm_auth(hass: HomeAssistant) -> None: + """Test the user initiated form for a relay switch 1pm.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_auth"} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_auth" + assert result["errors"] == {} + + with patch( + "switchbot.SwitchbotRelaySwitch.async_retrieve_encryption_key", + side_effect=SwitchbotAuthenticationError("error from api"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_auth" + assert result["errors"] == {"base": "auth_failed"} + assert "error from api" in result["description_placeholders"]["error_detail"] + + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "switchbot.SwitchbotRelaySwitch.async_retrieve_encryption_key", + return_value={ + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ), + patch( + "switchbot.SwitchbotRelaySwitch.verify_encryption_key", return_value=True + ), + ): + 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"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Relay Switch 1PM EEFF" + assert result["data"] == { + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + CONF_SENSOR_TYPE: "relay_switch_1pm", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_worelay_switch_1pm_auth_switchbot_api_down( + hass: HomeAssistant, +) -> None: + """Test the user initiated form for a relay switch 1pm when the switchbot api is down.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_auth"} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_auth" + assert result["errors"] == {} + + with patch( + "switchbot.SwitchbotRelaySwitch.async_retrieve_encryption_key", + side_effect=SwitchbotAccountConnectionError("Switchbot API down"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "", + CONF_PASSWORD: "", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "api_error" + assert result["description_placeholders"] == {"error_detail": "Switchbot API down"} diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index 3adeaef936c..6a7111a054e 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -1,9 +1,15 @@ """Test the switchbot sensors.""" +from unittest.mock import patch + import pytest from homeassistant.components.sensor import ATTR_STATE_CLASS -from homeassistant.components.switchbot.const import DOMAIN +from homeassistant.components.switchbot.const import ( + CONF_ENCRYPTION_KEY, + CONF_KEY_ID, + DOMAIN, +) from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -15,7 +21,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from . import WOHAND_SERVICE_INFO, WOMETERTHPC_SERVICE_INFO +from . import ( + LEAK_SERVICE_INFO, + REMOTE_SERVICE_INFO, + WOHAND_SERVICE_INFO, + WOMETERTHPC_SERVICE_INFO, + WORELAY_SWITCH_1PM_SERVICE_INFO, +) from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -105,3 +117,120 @@ async def test_co2_sensor(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_relay_switch_1pm_power_sensor(hass: HomeAssistant) -> None: + """Test setting up creates the power sensor.""" + await async_setup_component(hass, DOMAIN, {}) + inject_bluetooth_service_info(hass, WORELAY_SWITCH_1PM_SERVICE_INFO) + + with patch( + "switchbot.SwitchbotRelaySwitch.update", + return_value=None, + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_SENSOR_TYPE: "relay_switch_1pm", + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + unique_id="aabbccddeeaa", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + power_sensor = hass.states.get("sensor.test_name_power") + power_sensor_attrs = power_sensor.attributes + assert power_sensor.state == "4.9" + assert power_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Power" + assert power_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "W" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_leak_sensor(hass: HomeAssistant) -> None: + """Test setting up the leak detector.""" + await async_setup_component(hass, DOMAIN, {}) + inject_bluetooth_service_info(hass, LEAK_SERVICE_INFO) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_SENSOR_TYPE: "leak", + }, + unique_id="aabbccddeeaa", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + battery_sensor = hass.states.get("sensor.test_name_battery") + battery_sensor_attrs = battery_sensor.attributes + assert battery_sensor.state == "86" + assert battery_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Battery" + assert battery_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal") + rssi_sensor_attrs = rssi_sensor.attributes + assert rssi_sensor.state == "-60" + assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal" + assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm" + + leak_sensor = hass.states.get("binary_sensor.test_name") + leak_sensor_attrs = leak_sensor.attributes + assert leak_sensor.state == "off" + assert leak_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_remote(hass: HomeAssistant) -> None: + """Test setting up the remote sensor.""" + await async_setup_component(hass, DOMAIN, {}) + inject_bluetooth_service_info(hass, REMOTE_SERVICE_INFO) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_SENSOR_TYPE: "remote", + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 2 + + battery_sensor = hass.states.get("sensor.test_name_battery") + battery_sensor_attrs = battery_sensor.attributes + assert battery_sensor.state == "86" + assert battery_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Battery" + assert battery_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal") + rssi_sensor_attrs = rssi_sensor.attributes + assert rssi_sensor.state == "-60" + assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal" + assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/switchbot_cloud/__init__.py b/tests/components/switchbot_cloud/__init__.py index ce570499b3a..42fe3e4f543 100644 --- a/tests/components/switchbot_cloud/__init__.py +++ b/tests/components/switchbot_cloud/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -def configure_integration(hass: HomeAssistant) -> MockConfigEntry: +async def configure_integration(hass: HomeAssistant) -> MockConfigEntry: """Configure the integration.""" config = { CONF_API_TOKEN: "test-token", @@ -17,5 +17,7 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: domain=DOMAIN, data=config, entry_id="123456", unique_id="123456" ) entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/switchbot_cloud/fixtures/meter_status.json b/tests/components/switchbot_cloud/fixtures/meter_status.json new file mode 100644 index 00000000000..8b5bcd0c031 --- /dev/null +++ b/tests/components/switchbot_cloud/fixtures/meter_status.json @@ -0,0 +1,9 @@ +{ + "version": "V3.3", + "temperature": 21.8, + "battery": 100, + "humidity": 32, + "deviceId": "meter-id-1", + "deviceType": "Meter", + "hubDeviceId": "test-hub-id" +} diff --git a/tests/components/switchbot_cloud/snapshots/test_sensor.ambr b/tests/components/switchbot_cloud/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..2446add959b --- /dev/null +++ b/tests/components/switchbot_cloud/snapshots/test_sensor.ambr @@ -0,0 +1,313 @@ +# serializer version: 1 +# name: test_meter[sensor.meter_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_1_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'meter-id-1_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_meter[sensor.meter_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'meter-1 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.meter_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_meter[sensor.meter_1_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_1_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'meter-id-1_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_meter[sensor.meter_1_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'meter-1 Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.meter_1_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '32', + }) +# --- +# name: test_meter[sensor.meter_1_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_1_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'meter-id-1_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_meter[sensor.meter_1_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'meter-1 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.meter_1_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.8', + }) +# --- +# name: test_meter_no_coordinator_data[sensor.meter_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_1_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'meter-id-1_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_meter_no_coordinator_data[sensor.meter_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'meter-1 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.meter_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_meter_no_coordinator_data[sensor.meter_1_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_1_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'meter-id-1_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_meter_no_coordinator_data[sensor.meter_1_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'meter-1 Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.meter_1_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_meter_no_coordinator_data[sensor.meter_1_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.meter_1_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'meter-id-1_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_meter_no_coordinator_data[sensor.meter_1_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'meter-1 Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.meter_1_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/switchbot_cloud/test_button.py b/tests/components/switchbot_cloud/test_button.py new file mode 100644 index 00000000000..0779e54ee03 --- /dev/null +++ b/tests/components/switchbot_cloud/test_button.py @@ -0,0 +1,65 @@ +"""Test for the switchbot_cloud bot as a button.""" + +from unittest.mock import patch + +from switchbot_api import BotCommands, Device + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.switchbot_cloud import SwitchBotAPI +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_pressmode_bot( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test press.""" + mock_list_devices.return_value = [ + Device( + deviceId="bot-id-1", + deviceName="bot-1", + deviceType="Bot", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.return_value = {"deviceMode": "pressMode"} + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + entity_id = "button.bot_1" + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_send_command.assert_called_once_with( + "bot-id-1", BotCommands.PRESS, "command", "default" + ) + + assert hass.states.get(entity_id).state != STATE_UNKNOWN + + +async def test_switchmode_bot_no_button_entity( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test a switchMode bot isn't added as a button.""" + mock_list_devices.return_value = [ + Device( + deviceId="bot-id-1", + deviceName="bot-1", + deviceType="Bot", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.return_value = {"deviceMode": "switchMode"} + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + assert not hass.states.async_entity_ids(BUTTON_DOMAIN) diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index 43431ae04c0..f4837c4e97e 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -64,9 +64,7 @@ async def test_setup_entry_success( ), ] mock_get_status.return_value = {"power": PowerState.ON.value} - entry = configure_integration(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + entry = await configure_integration(hass) assert entry.state is ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -91,8 +89,7 @@ async def test_setup_entry_fails_when_listing_devices( ) -> None: """Test error handling when list_devices in setup of entry.""" mock_list_devices.side_effect = error - entry = configure_integration(hass) - await hass.config_entries.async_setup(entry.entry_id) + entry = await configure_integration(hass) assert entry.state == state hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -114,9 +111,8 @@ async def test_setup_entry_fails_when_refreshing( ) ] mock_get_status.side_effect = CannotConnect - entry = configure_integration(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.LOADED + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/switchbot_cloud/test_lock.py b/tests/components/switchbot_cloud/test_lock.py index a09d7241794..fcb81abfc51 100644 --- a/tests/components/switchbot_cloud/test_lock.py +++ b/tests/components/switchbot_cloud/test_lock.py @@ -26,9 +26,7 @@ async def test_lock(hass: HomeAssistant, mock_list_devices, mock_get_status) -> mock_get_status.return_value = {"lockState": "locked"} - entry = configure_integration(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + entry = await configure_integration(hass) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/switchbot_cloud/test_sensor.py b/tests/components/switchbot_cloud/test_sensor.py new file mode 100644 index 00000000000..6b0a52800f3 --- /dev/null +++ b/tests/components/switchbot_cloud/test_sensor.py @@ -0,0 +1,65 @@ +"""Test for the switchbot_cloud sensors.""" + +from unittest.mock import patch + +from switchbot_api import Device +from syrupy import SnapshotAssertion + +from homeassistant.components.switchbot_cloud.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import configure_integration + +from tests.common import load_json_object_fixture, snapshot_platform + + +async def test_meter( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_list_devices, + mock_get_status, +) -> None: + """Test Meter sensors.""" + + mock_list_devices.return_value = [ + Device( + deviceId="meter-id-1", + deviceName="meter-1", + deviceType="Meter", + hubDeviceId="test-hub-id", + ), + ] + mock_get_status.return_value = load_json_object_fixture("meter_status.json", DOMAIN) + + with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]): + entry = await configure_integration(hass) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +async def test_meter_no_coordinator_data( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_list_devices, + mock_get_status, +) -> None: + """Test meter sensors are unknown without coordinator data.""" + mock_list_devices.return_value = [ + Device( + deviceId="meter-id-1", + deviceName="meter-1", + deviceType="Meter", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.return_value = None + + with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]): + entry = await configure_integration(hass) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) diff --git a/tests/components/switchbot_cloud/test_switch.py b/tests/components/switchbot_cloud/test_switch.py new file mode 100644 index 00000000000..99e0f50aa53 --- /dev/null +++ b/tests/components/switchbot_cloud/test_switch.py @@ -0,0 +1,107 @@ +"""Test for the switchbot_cloud relay switch & bot.""" + +from unittest.mock import patch + +from switchbot_api import Device + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switchbot_cloud import SwitchBotAPI +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +async def test_relay_switch( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test turn on and turn off.""" + mock_list_devices.return_value = [ + Device( + deviceId="relay-switch-id-1", + deviceName="relay-switch-1", + deviceType="Relay Switch 1", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.return_value = {"switchStatus": 0} + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + entity_id = "switch.relay_switch_1" + assert hass.states.get(entity_id).state == STATE_OFF + + with patch.object(SwitchBotAPI, "send_command"): + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + + with patch.object(SwitchBotAPI, "send_command"): + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_switchmode_bot( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test turn on and turn off.""" + mock_list_devices.return_value = [ + Device( + deviceId="bot-id-1", + deviceName="bot-1", + deviceType="Bot", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.return_value = {"deviceMode": "switchMode", "power": "off"} + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + entity_id = "switch.bot_1" + assert hass.states.get(entity_id).state == STATE_OFF + + with patch.object(SwitchBotAPI, "send_command"): + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + + with patch.object(SwitchBotAPI, "send_command"): + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_pressmode_bot_no_switch_entity( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test a pressMode bot isn't added as a switch.""" + mock_list_devices.return_value = [ + Device( + deviceId="bot-id-1", + deviceName="bot-1", + deviceType="Bot", + hubDeviceId="test-hub-id", + ), + ] + + mock_get_status.return_value = {"deviceMode": "pressMode"} + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + assert not hass.states.async_entity_ids(SWITCH_DOMAIN) diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index defe970c674..57454e38062 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -91,8 +91,8 @@ DUMMY_POSITION = [54] DUMMY_POSITION_2 = [54, 54] DUMMY_DIRECTION = [ShutterDirection.SHUTTER_STOP] DUMMY_DIRECTION_2 = [ShutterDirection.SHUTTER_STOP, ShutterDirection.SHUTTER_STOP] -DUMMY_CHILD_LOCK = [ShutterChildLock.OFF] -DUMMY_CHILD_LOCK_2 = [ShutterChildLock.OFF, ShutterChildLock.OFF] +DUMMY_CHILD_LOCK = [ShutterChildLock.ON] +DUMMY_CHILD_LOCK_2 = [ShutterChildLock.ON, ShutterChildLock.ON] DUMMY_USERNAME = "email" DUMMY_TOKEN = "zvVvd7JxtN7CgvkD1Psujw==" DUMMY_LIGHT = [DeviceState.ON] diff --git a/tests/components/switcher_kis/test_switch.py b/tests/components/switcher_kis/test_switch.py index 9bfe11fe202..c20149de074 100644 --- a/tests/components/switcher_kis/test_switch.py +++ b/tests/components/switcher_kis/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from aioswitcher.api import Command, SwitcherBaseResponse +from aioswitcher.api import Command, ShutterChildLock, SwitcherBaseResponse from aioswitcher.device import DeviceState import pytest @@ -20,7 +20,20 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.util import slugify from . import init_integration -from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE +from .consts import ( + DUMMY_DUAL_SHUTTER_SINGLE_LIGHT_DEVICE as DEVICE3, + DUMMY_PLUG_DEVICE, + DUMMY_SHUTTER_DEVICE as DEVICE, + DUMMY_SINGLE_SHUTTER_DUAL_LIGHT_DEVICE as DEVICE2, + DUMMY_TOKEN as TOKEN, + DUMMY_USERNAME as USERNAME, + DUMMY_WATER_HEATER_DEVICE, +) + +ENTITY_ID = f"{SWITCH_DOMAIN}.{slugify(DEVICE.name)}_child_lock" +ENTITY_ID2 = f"{SWITCH_DOMAIN}.{slugify(DEVICE2.name)}_child_lock" +ENTITY_ID3 = f"{SWITCH_DOMAIN}.{slugify(DEVICE3.name)}_child_lock_1" +ENTITY_ID3_2 = f"{SWITCH_DOMAIN}.{slugify(DEVICE3.name)}_child_lock_2" @pytest.mark.parametrize("mock_bridge", [[DUMMY_WATER_HEATER_DEVICE]], indirect=True) @@ -137,3 +150,192 @@ async def test_switch_control_fail( mock_control_device.assert_called_once_with(Command.ON) state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ( + "device", + "entity_id", + "cover_id", + "child_lock_state", + ), + [ + ( + DEVICE, + ENTITY_ID, + 0, + [ShutterChildLock.OFF], + ), + ( + DEVICE2, + ENTITY_ID2, + 0, + [ShutterChildLock.OFF], + ), + ( + DEVICE3, + ENTITY_ID3, + 0, + [ShutterChildLock.OFF, ShutterChildLock.ON], + ), + ( + DEVICE3, + ENTITY_ID3_2, + 1, + [ShutterChildLock.ON, ShutterChildLock.OFF], + ), + ], +) +@pytest.mark.parametrize("mock_bridge", [[DEVICE, DEVICE2, DEVICE3]], indirect=True) +async def test_child_lock_switch( + hass: HomeAssistant, + mock_bridge, + mock_api, + monkeypatch: pytest.MonkeyPatch, + device, + entity_id: str, + cover_id: int, + child_lock_state: list[ShutterChildLock], +) -> None: + """Test the switch.""" + await init_integration(hass, USERNAME, TOKEN) + assert mock_bridge + + # Test initial state - on + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test state change on --> off + monkeypatch.setattr(device, "child_lock", child_lock_state) + mock_bridge.mock_callbacks([device]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Test turning on child lock + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.set_shutter_child_lock", + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(ShutterChildLock.ON, cover_id) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Test turning off + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.set_shutter_child_lock" + ) as mock_control_device: + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with(ShutterChildLock.OFF, cover_id) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + ( + "device", + "entity_id", + "cover_id", + "child_lock_state", + ), + [ + ( + DEVICE, + ENTITY_ID, + 0, + [ShutterChildLock.OFF], + ), + ( + DEVICE2, + ENTITY_ID2, + 0, + [ShutterChildLock.OFF], + ), + ( + DEVICE3, + ENTITY_ID3, + 0, + [ShutterChildLock.OFF, ShutterChildLock.ON], + ), + ( + DEVICE3, + ENTITY_ID3_2, + 1, + [ShutterChildLock.ON, ShutterChildLock.OFF], + ), + ], +) +@pytest.mark.parametrize("mock_bridge", [[DEVICE, DEVICE2, DEVICE3]], indirect=True) +async def test_child_lock_control_fail( + hass: HomeAssistant, + mock_bridge, + mock_api, + monkeypatch: pytest.MonkeyPatch, + device, + entity_id: str, + cover_id: int, + child_lock_state: list[ShutterChildLock], +) -> None: + """Test switch control fail.""" + await init_integration(hass, USERNAME, TOKEN) + assert mock_bridge + + # Test initial state - off + monkeypatch.setattr(device, "child_lock", child_lock_state) + mock_bridge.mock_callbacks([device]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Test exception during turn on + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.set_shutter_child_lock", + side_effect=RuntimeError("fake error"), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_api.call_count == 2 + mock_control_device.assert_called_once_with(ShutterChildLock.ON, cover_id) + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + # Make device available again + mock_bridge.mock_callbacks([device]) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + # Test error response during turn on + with patch( + "homeassistant.components.switcher_kis.entity.SwitcherApi.set_shutter_child_lock", + return_value=SwitcherBaseResponse(None), + ) as mock_control_device: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_api.call_count == 4 + mock_control_device.assert_called_once_with(ShutterChildLock.ON, cover_id) + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index b79e63e1ce7..727b95563cc 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -6,12 +6,19 @@ from unittest.mock import patch from pysyncthru import SyncThruAPINotSupported from homeassistant import config_entries -from homeassistant.components import ssdp from homeassistant.components.syncthru.config_flow import SyncThru from homeassistant.components.syncthru.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_DEVICE_TYPE, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_PRESENTATION_URL, + ATTR_UPNP_SERIAL, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -138,16 +145,16 @@ async def test_ssdp(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://192.168.1.2:5200/Printer.xml", upnp={ - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:Printer:1", - ssdp.ATTR_UPNP_MANUFACTURER: "Samsung Electronics", - ssdp.ATTR_UPNP_PRESENTATION_URL: url, - ssdp.ATTR_UPNP_SERIAL: "00000000", - ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:Printer:1", + ATTR_UPNP_MANUFACTURER: "Samsung Electronics", + ATTR_UPNP_PRESENTATION_URL: url, + ATTR_UPNP_SERIAL: "00000000", + ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", }, ), ) diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 0e8f79ffd40..331c879332d 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -34,5 +34,5 @@ def fixture_dsm(): dsm.network.update = AsyncMock(return_value=True) dsm.surveillance_station.update = AsyncMock(return_value=True) dsm.upgrade.update = AsyncMock(return_value=True) - + dsm.file = AsyncMock(get_shared_folders=AsyncMock(return_value=None)) return dsm diff --git a/tests/components/synology_dsm/snapshots/test_config_flow.ambr b/tests/components/synology_dsm/snapshots/test_config_flow.ambr index 807ec764e52..384f6b885d7 100644 --- a/tests/components/synology_dsm/snapshots/test_config_flow.ambr +++ b/tests/components/synology_dsm/snapshots/test_config_flow.ambr @@ -84,3 +84,17 @@ 'verify_ssl': False, }) # --- +# name: test_user_with_filestation + dict({ + 'host': 'nas.meontheinternet.com', + 'mac': list([ + '00-11-32-XX-XX-59', + '00-11-32-XX-XX-5A', + ]), + 'password': 'password', + 'port': 1234, + 'ssl': True, + 'username': 'Home_Assistant', + 'verify_ssl': False, + }) +# --- diff --git a/tests/components/synology_dsm/test_backup.py b/tests/components/synology_dsm/test_backup.py new file mode 100644 index 00000000000..ea68bbc991c --- /dev/null +++ b/tests/components/synology_dsm/test_backup.py @@ -0,0 +1,752 @@ +"""Tests for the Synology DSM backup agent.""" + +from io import StringIO +from typing import Any +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch + +import pytest +from synology_dsm.api.file_station.models import SynoFileFile, SynoFileSharedFolder +from synology_dsm.exceptions import SynologyDSMAPIErrorException + +from homeassistant.components.backup import ( + DOMAIN as BACKUP_DOMAIN, + AddonInfo, + AgentBackup, + Folder, +) +from homeassistant.components.synology_dsm.const import ( + CONF_BACKUP_PATH, + CONF_BACKUP_SHARE, + DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.aiohttp import MockStreamReader + +from .consts import HOST, MACS, PASSWORD, PORT, SERIAL, USE_SSL, USERNAME + +from tests.common import MockConfigEntry +from tests.typing import ClientSessionGenerator, WebSocketGenerator + +BASE_FILENAME = "Automatic_backup_2025.2.0.dev0_2025-01-09_20.14_35457323" + + +class MockStreamReaderChunked(MockStreamReader): + """Mock a stream reader with simulated chunked data.""" + + async def readchunk(self) -> tuple[bytes, bool]: + """Read bytes.""" + return (self._content.read(), False) + + +async def _mock_download_file(path: str, filename: str) -> MockStreamReader: + if filename == f"{BASE_FILENAME}_meta.json": + return MockStreamReader( + b'{"addons":[],"backup_id":"abcd12ef","date":"2025-01-09T20:14:35.457323+01:00",' + b'"database_included":true,"extra_metadata":{"instance_id":"36b3b7e984da43fc89f7bafb2645fa36",' + b'"with_automatic_settings":true},"folders":[],"homeassistant_included":true,' + b'"homeassistant_version":"2025.2.0.dev0","name":"Automatic backup 2025.2.0.dev0","protected":true,"size":13916160}' + ) + if filename == f"{BASE_FILENAME}.tar": + return MockStreamReaderChunked(b"backup data") + raise MockStreamReaderChunked(b"") + + +async def _mock_download_file_meta_ok_tar_missing( + path: str, filename: str +) -> MockStreamReader: + if filename == f"{BASE_FILENAME}_meta.json": + return MockStreamReader( + b'{"addons":[],"backup_id":"abcd12ef","date":"2025-01-09T20:14:35.457323+01:00",' + b'"database_included":true,"extra_metadata":{"instance_id":"36b3b7e984da43fc89f7bafb2645fa36",' + b'"with_automatic_settings":true},"folders":[],"homeassistant_included":true,' + b'"homeassistant_version":"2025.2.0.dev0","name":"Automatic backup 2025.2.0.dev0","protected":true,"size":13916160}' + ) + if filename == f"{BASE_FILENAME}.tar": + raise SynologyDSMAPIErrorException("api", "900", [{"code": 408}]) + raise MockStreamReaderChunked(b"") + + +async def _mock_download_file_meta_defect(path: str, filename: str) -> MockStreamReader: + if filename == f"{BASE_FILENAME}_meta.json": + return MockStreamReader(b"im not a json") + if filename == f"{BASE_FILENAME}.tar": + return MockStreamReaderChunked(b"backup data") + raise MockStreamReaderChunked(b"") + + +@pytest.fixture +def mock_dsm_with_filestation(): + """Mock a successful service with filestation support.""" + with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm: + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + dsm.file = AsyncMock( + get_shared_folders=AsyncMock( + return_value=[ + SynoFileSharedFolder( + additional=None, + is_dir=True, + name="HA Backup", + path="/ha_backup", + ) + ] + ), + get_files=AsyncMock( + return_value=[ + SynoFileFile( + additional=None, + is_dir=False, + name=f"{BASE_FILENAME}_meta.json", + path=f"/ha_backup/my_backup_path/{BASE_FILENAME}_meta.json", + ), + SynoFileFile( + additional=None, + is_dir=False, + name=f"{BASE_FILENAME}.tar", + path=f"/ha_backup/my_backup_path/{BASE_FILENAME}.tar", + ), + ] + ), + download_file=_mock_download_file, + upload_file=AsyncMock(return_value=True), + delete_file=AsyncMock(return_value=True), + ) + dsm.logout = AsyncMock(return_value=True) + yield dsm + + +@pytest.fixture +def mock_dsm_without_filestation(): + """Mock a successful service with filestation support.""" + + with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm: + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + dsm.file = None + + yield dsm + + +@pytest.fixture +async def setup_dsm_with_filestation( + hass: HomeAssistant, + mock_dsm_with_filestation: MagicMock, +): + """Mock setup of synology dsm config entry.""" + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=mock_dsm_with_filestation, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS[0], + }, + options={ + CONF_BACKUP_PATH: "my_backup_path", + CONF_BACKUP_SHARE: "/ha_backup", + }, + unique_id="mocked_syno_dsm_entry", + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}}) + await hass.async_block_till_done() + + yield mock_dsm_with_filestation + + +async def test_agents_info( + hass: HomeAssistant, + setup_dsm_with_filestation: MagicMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test backup agent info.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/agents/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agents": [ + {"agent_id": "synology_dsm.mocked_syno_dsm_entry", "name": "Mock Title"}, + {"agent_id": "backup.local", "name": "local"}, + ], + } + + +async def test_agents_not_loaded( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test backup agent with no loaded config entry.""" + with patch("homeassistant.components.backup.is_hassio", return_value=False): + assert await async_setup_component(hass, BACKUP_DOMAIN, {BACKUP_DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/agents/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agents": [ + {"agent_id": "backup.local", "name": "local"}, + ], + } + + +async def test_agents_on_unload( + hass: HomeAssistant, + setup_dsm_with_filestation: MagicMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test backup agent on un-loading config entry.""" + # config entry is loaded + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/agents/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agents": [ + {"agent_id": "synology_dsm.mocked_syno_dsm_entry", "name": "Mock Title"}, + {"agent_id": "backup.local", "name": "local"}, + ], + } + + # unload config entry + entries = hass.config_entries.async_loaded_entries(DOMAIN) + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/agents/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agents": [ + {"agent_id": "backup.local", "name": "local"}, + ], + } + + +async def test_agents_list_backups( + hass: HomeAssistant, + setup_dsm_with_filestation: MagicMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test agent list backups.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["agent_errors"] == {} + assert response["result"]["backups"] == [ + { + "addons": [], + "agents": { + "synology_dsm.mocked_syno_dsm_entry": { + "protected": True, + "size": 13916160, + } + }, + "backup_id": "abcd12ef", + "date": "2025-01-09T20:14:35.457323+01:00", + "database_included": True, + "extra_metadata": {"instance_id": ANY, "with_automatic_settings": True}, + "folders": [], + "homeassistant_included": True, + "homeassistant_version": "2025.2.0.dev0", + "name": "Automatic backup 2025.2.0.dev0", + "failed_agent_ids": [], + "with_automatic_settings": None, + } + ] + + +async def test_agents_list_backups_error( + hass: HomeAssistant, + setup_dsm_with_filestation: MagicMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test agent error while list backups.""" + client = await hass_ws_client(hass) + + setup_dsm_with_filestation.file.get_files.side_effect = ( + SynologyDSMAPIErrorException("api", "500", "error") + ) + + await client.send_json_auto_id({"type": "backup/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agent_errors": { + "synology_dsm.mocked_syno_dsm_entry": "Failed to list backups" + }, + "backups": [], + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "last_non_idle_event": None, + "next_automatic_backup": None, + "next_automatic_backup_additional": False, + "state": "idle", + } + + +async def test_agents_list_backups_disabled_filestation( + hass: HomeAssistant, + mock_dsm_without_filestation: MagicMock, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test agent error while list backups when file station is disabled.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "backup/info"}) + response = await client.receive_json() + + assert not response["success"] + + +@pytest.mark.parametrize( + ("backup_id", "expected_result"), + [ + ( + "abcd12ef", + { + "addons": [], + "agents": { + "synology_dsm.mocked_syno_dsm_entry": { + "protected": True, + "size": 13916160, + } + }, + "backup_id": "abcd12ef", + "date": "2025-01-09T20:14:35.457323+01:00", + "database_included": True, + "extra_metadata": {"instance_id": ANY, "with_automatic_settings": True}, + "folders": [], + "homeassistant_included": True, + "homeassistant_version": "2025.2.0.dev0", + "name": "Automatic backup 2025.2.0.dev0", + "failed_agent_ids": [], + "with_automatic_settings": None, + }, + ), + ( + "12345", + None, + ), + ], + ids=["found", "not_found"], +) +async def test_agents_get_backup( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_dsm_with_filestation: MagicMock, + backup_id: str, + expected_result: dict[str, Any] | None, +) -> None: + """Test agent get backup.""" + client = await hass_ws_client(hass) + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"]["agent_errors"] == {} + assert response["result"]["backup"] == expected_result + + +async def test_agents_get_backup_not_existing( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent get not existing backup.""" + client = await hass_ws_client(hass) + backup_id = "ef34ab12" + + setup_dsm_with_filestation.file.download_file = AsyncMock( + side_effect=SynologyDSMAPIErrorException("api", "404", "not found") + ) + + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"agent_errors": {}, "backup": None} + + +async def test_agents_get_backup_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent error while get backup.""" + client = await hass_ws_client(hass) + backup_id = "ef34ab12" + + setup_dsm_with_filestation.file.get_files.side_effect = ( + SynologyDSMAPIErrorException("api", "500", "error") + ) + + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agent_errors": { + "synology_dsm.mocked_syno_dsm_entry": "Failed to list backups" + }, + "backup": None, + } + + +async def test_agents_get_backup_defect_meta( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent error while get backup.""" + client = await hass_ws_client(hass) + backup_id = "ef34ab12" + + setup_dsm_with_filestation.file.download_file = _mock_download_file_meta_defect + + await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"agent_errors": {}, "backup": None} + + +async def test_agents_download( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent download backup.""" + client = await hass_client() + backup_id = "abcd12ef" + + resp = await client.get( + f"/api/backup/download/{backup_id}?agent_id=synology_dsm.mocked_syno_dsm_entry" + ) + assert resp.status == 200 + assert await resp.content.read() == b"backup data" + + +async def test_agents_download_not_existing( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent download not existing backup.""" + client = await hass_client() + backup_id = "abcd12ef" + + setup_dsm_with_filestation.file.download_file = ( + _mock_download_file_meta_ok_tar_missing + ) + + resp = await client.get( + f"/api/backup/download/{backup_id}?agent_id=synology_dsm.mocked_syno_dsm_entry" + ) + assert resp.reason == "Internal Server Error" + assert resp.status == 500 + + +async def test_agents_upload( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent upload backup.""" + client = await hass_client() + backup_id = "test-backup" + test_backup = AgentBackup( + addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], + backup_id=backup_id, + database_included=True, + date="1970-01-01T00:00:00.000Z", + extra_metadata={}, + folders=[Folder.MEDIA, Folder.SHARE], + homeassistant_included=True, + homeassistant_version="2024.12.0", + name="Test", + protected=True, + size=0, + ) + base_filename = "Test_1970-01-01_00.00_00000000" + + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) + fetch_backup.return_value = test_backup + resp = await client.post( + "/api/backup/upload?agent_id=synology_dsm.mocked_syno_dsm_entry", + data={"file": StringIO("test")}, + ) + + assert resp.status == 201 + assert f"Uploading backup {backup_id}" in caplog.text + mock: AsyncMock = setup_dsm_with_filestation.file.upload_file + assert len(mock.mock_calls) == 2 + assert mock.call_args_list[0].kwargs["filename"] == f"{base_filename}.tar" + assert mock.call_args_list[0].kwargs["path"] == "/ha_backup/my_backup_path" + assert mock.call_args_list[1].kwargs["filename"] == f"{base_filename}_meta.json" + assert mock.call_args_list[1].kwargs["path"] == "/ha_backup/my_backup_path" + + +async def test_agents_upload_error( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent error while uploading backup.""" + client = await hass_client() + backup_id = "test-backup" + test_backup = AgentBackup( + addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], + backup_id=backup_id, + database_included=True, + date="1970-01-01T00:00:00.000Z", + extra_metadata={}, + folders=[Folder.MEDIA, Folder.SHARE], + homeassistant_included=True, + homeassistant_version="2024.12.0", + name="Test", + protected=True, + size=0, + ) + base_filename = "Test_1970-01-01_00.00_00000000" + + # fail to upload the tar file + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) + fetch_backup.return_value = test_backup + setup_dsm_with_filestation.file.upload_file.side_effect = ( + SynologyDSMAPIErrorException("api", "500", "error") + ) + resp = await client.post( + "/api/backup/upload?agent_id=synology_dsm.mocked_syno_dsm_entry", + data={"file": StringIO("test")}, + ) + + assert resp.status == 201 + assert f"Uploading backup {backup_id}" in caplog.text + assert "Failed to upload backup" in caplog.text + mock: AsyncMock = setup_dsm_with_filestation.file.upload_file + assert len(mock.mock_calls) == 1 + assert mock.call_args_list[0].kwargs["filename"] == f"{base_filename}.tar" + assert mock.call_args_list[0].kwargs["path"] == "/ha_backup/my_backup_path" + + # fail to upload the meta json file + with ( + patch( + "homeassistant.components.backup.manager.BackupManager.async_get_backup", + ) as fetch_backup, + patch( + "homeassistant.components.backup.manager.read_backup", + return_value=test_backup, + ), + patch("pathlib.Path.open") as mocked_open, + ): + mocked_open.return_value.read = Mock(side_effect=[b"test", b""]) + fetch_backup.return_value = test_backup + setup_dsm_with_filestation.file.upload_file.side_effect = [ + True, + SynologyDSMAPIErrorException("api", "500", "error"), + ] + + resp = await client.post( + "/api/backup/upload?agent_id=synology_dsm.mocked_syno_dsm_entry", + data={"file": StringIO("test")}, + ) + + assert resp.status == 201 + assert f"Uploading backup {backup_id}" in caplog.text + assert "Failed to upload backup" in caplog.text + mock: AsyncMock = setup_dsm_with_filestation.file.upload_file + assert len(mock.mock_calls) == 3 + assert mock.call_args_list[1].kwargs["filename"] == f"{base_filename}.tar" + assert mock.call_args_list[1].kwargs["path"] == "/ha_backup/my_backup_path" + assert mock.call_args_list[2].kwargs["filename"] == f"{base_filename}_meta.json" + assert mock.call_args_list[2].kwargs["path"] == "/ha_backup/my_backup_path" + + +async def test_agents_delete( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test agent delete backup.""" + client = await hass_ws_client(hass) + backup_id = "abcd12ef" + + await client.send_json_auto_id( + { + "type": "backup/delete", + "backup_id": backup_id, + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"agent_errors": {}} + mock: AsyncMock = setup_dsm_with_filestation.file.delete_file + assert len(mock.mock_calls) == 2 + assert mock.call_args_list[0].kwargs["filename"] == f"{BASE_FILENAME}.tar" + assert mock.call_args_list[0].kwargs["path"] == "/ha_backup/my_backup_path" + assert mock.call_args_list[1].kwargs["filename"] == f"{BASE_FILENAME}_meta.json" + assert mock.call_args_list[1].kwargs["path"] == "/ha_backup/my_backup_path" + + +async def test_agents_delete_not_existing( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_dsm_with_filestation: MagicMock, +) -> None: + """Test delete not existing backup.""" + client = await hass_ws_client(hass) + backup_id = "ef34ab12" + + setup_dsm_with_filestation.file.download_file = ( + _mock_download_file_meta_ok_tar_missing + ) + setup_dsm_with_filestation.file.delete_file = AsyncMock( + side_effect=SynologyDSMAPIErrorException( + "api", + "900", + [{"code": 408, "path": f"/ha_backup/my_backup_path/{backup_id}.tar"}], + ) + ) + + await client.send_json_auto_id( + { + "type": "backup/delete", + "backup_id": backup_id, + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"agent_errors": {}} + + +@pytest.mark.parametrize( + ("error", "expected_log"), + [ + ( + SynologyDSMAPIErrorException("api", "100", "Unknown error"), + "{'api': 'api', 'code': '100', 'reason': 'Unknown', 'details': 'Unknown error'}", + ), + ( + SynologyDSMAPIErrorException("api", "900", [{"code": 407}]), + "{'api': 'api', 'code': '900', 'reason': 'Unknown', 'details': [{'code': 407}]", + ), + ( + SynologyDSMAPIErrorException("api", "900", [{"code": 417}]), + "{'api': 'api', 'code': '900', 'reason': 'Unknown', 'details': [{'code': 417}]", + ), + ], +) +async def test_agents_delete_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + caplog: pytest.LogCaptureFixture, + setup_dsm_with_filestation: MagicMock, + error: SynologyDSMAPIErrorException, + expected_log: str, +) -> None: + """Test error while delete backup.""" + client = await hass_ws_client(hass) + + # error while delete + backup_id = "abcd12ef" + setup_dsm_with_filestation.file.delete_file.side_effect = error + await client.send_json_auto_id( + { + "type": "backup/delete", + "backup_id": backup_id, + } + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == { + "agent_errors": { + "synology_dsm.mocked_syno_dsm_entry": "Failed to delete backup" + } + } + assert f"Failed to delete backup: {expected_log}" in caplog.text + mock: AsyncMock = setup_dsm_with_filestation.file.delete_file + assert len(mock.mock_calls) == 1 + assert mock.call_args_list[0].kwargs["filename"] == f"{BASE_FILENAME}.tar" + assert mock.call_args_list[0].kwargs["path"] == "/ha_backup/my_backup_path" diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index e5494b7179f..b25cf7a81ac 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -4,6 +4,7 @@ from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest +from synology_dsm.api.file_station.models import SynoFileSharedFolder from synology_dsm.exceptions import ( SynologyDSMException, SynologyDSMLogin2SAFailedException, @@ -13,12 +14,11 @@ from synology_dsm.exceptions import ( ) from syrupy import SnapshotAssertion -from homeassistant.components import ssdp, zeroconf from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( + CONF_BACKUP_PATH, + CONF_BACKUP_SHARE, CONF_SNAPSHOT_QUALITY, - DEFAULT_SCAN_INTERVAL, - DEFAULT_SNAPSHOT_QUALITY, DOMAIN, ) from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER, SOURCE_ZEROCONF @@ -27,13 +27,18 @@ from homeassistant.const import ( CONF_MAC, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .consts import ( DEVICE_TOKEN, @@ -68,7 +73,7 @@ def mock_controller_service(): update=AsyncMock(return_value=True), ) dsm.information = Mock(serial=SERIAL) - + dsm.file = AsyncMock(get_shared_folders=AsyncMock(return_value=None)) yield dsm @@ -91,6 +96,7 @@ def mock_controller_service_2sa(): update=AsyncMock(return_value=True), ) dsm.information = Mock(serial=SERIAL) + dsm.file = AsyncMock(get_shared_folders=AsyncMock(return_value=None)) yield dsm @@ -111,6 +117,39 @@ def mock_controller_service_vdsm(): update=AsyncMock(return_value=True), ) dsm.information = Mock(serial=SERIAL) + dsm.file = AsyncMock(get_shared_folders=AsyncMock(return_value=None)) + yield dsm + + +@pytest.fixture(name="service_with_filestation") +def mock_controller_service_with_filestation(): + """Mock a successful service with filestation support.""" + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + dsm.file = AsyncMock( + get_shared_folders=AsyncMock( + return_value=[ + SynoFileSharedFolder( + additional=None, + is_dir=True, + name="HA Backup", + path="/ha_backup", + ) + ] + ) + ) yield dsm @@ -132,7 +171,7 @@ def mock_controller_service_failed(): update=AsyncMock(return_value=True), ) dsm.information = Mock(serial=None) - + dsm.file = AsyncMock(get_shared_folders=AsyncMock(return_value=None)) yield dsm @@ -278,6 +317,55 @@ async def test_user_vdsm( assert result["data"] == snapshot +@pytest.mark.usefixtures("mock_setup_entry") +async def test_user_with_filestation( + hass: HomeAssistant, + service_with_filestation: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test user config.""" + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_with_filestation, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_with_filestation, + ): + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "backup_share" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_BACKUP_SHARE: "/ha_backup", CONF_BACKUP_PATH: "automatic_ha_backups"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == SERIAL + assert result["title"] == HOST + assert result["data"] == snapshot + + @pytest.mark.usefixtures("mock_setup_entry") async def test_reauth(hass: HomeAssistant, service: MagicMock) -> None: """Test reauthentication.""" @@ -418,13 +506,13 @@ async def test_form_ssdp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://192.168.1.5:5000", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX99", # MAC address, but SSDP does not have `-` + ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ATTR_UPNP_SERIAL: "001132XXXX99", # MAC address, but SSDP does not have `-` }, ), ) @@ -465,13 +553,13 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://192.168.1.5:5000", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` }, ), ) @@ -508,13 +596,13 @@ async def test_skip_reconfig_ssdp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{new_host}:5000", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` }, ), ) @@ -541,13 +629,13 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://192.168.1.5:5000", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", - ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` }, ), ) @@ -555,46 +643,52 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock) -> None: assert result["reason"] == "already_configured" -@pytest.mark.usefixtures("mock_setup_entry") -async def test_options_flow(hass: HomeAssistant, service: MagicMock) -> None: +async def test_options_flow( + hass: HomeAssistant, service_with_filestation: MagicMock +) -> None: """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_MAC: MACS, - }, - unique_id=SERIAL, - ) - config_entry.add_to_hass(hass) + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=service_with_filestation, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS[0], + }, + unique_id=SERIAL, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() - assert config_entry.options == {} + assert config_entry.options == {CONF_BACKUP_SHARE: None, CONF_BACKUP_PATH: None} result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - # Scan interval - # Default - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={}, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL - assert config_entry.options[CONF_SNAPSHOT_QUALITY] == DEFAULT_SNAPSHOT_QUALITY - - # Manual result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_SCAN_INTERVAL: 2, CONF_SNAPSHOT_QUALITY: 0}, + user_input={ + CONF_SNAPSHOT_QUALITY: 0, + CONF_BACKUP_PATH: "my_nackup_path", + CONF_BACKUP_SHARE: "/ha_backup", + }, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 + assert config_entry.options[CONF_BACKUP_PATH] == "my_nackup_path" + assert config_entry.options[CONF_BACKUP_SHARE] == "/ha_backup" @pytest.mark.usefixtures("mock_setup_entry") @@ -606,7 +700,7 @@ async def test_discovered_via_zeroconf( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.5"), ip_addresses=[ip_address("192.168.1.5")], port=5000, @@ -645,7 +739,7 @@ async def test_discovered_via_zeroconf_missing_mac( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.5"), ip_addresses=[ip_address("192.168.1.5")], port=5000, diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 13d568e6137..7fe58719aa4 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -4,14 +4,22 @@ from unittest.mock import MagicMock, patch from synology_dsm.exceptions import SynologyDSMLoginInvalidException -from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES +from homeassistant.components.synology_dsm.const import ( + CONF_BACKUP_PATH, + CONF_BACKUP_SHARE, + DEFAULT_VERIFY_SSL, + DOMAIN, + SERVICES, +) from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -78,3 +86,40 @@ async def test_reauth_triggered(hass: HomeAssistant) -> None: assert not await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_async_step_reauth.assert_called_once() + + +async def test_config_entry_migrations( + hass: HomeAssistant, mock_dsm: MagicMock +) -> None: + """Test if reauthentication flow is triggered.""" + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=mock_dsm, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS[0], + }, + options={CONF_SCAN_INTERVAL: 30}, + ) + entry.add_to_hass(hass) + + assert CONF_VERIFY_SSL not in entry.data + assert CONF_BACKUP_SHARE not in entry.options + assert CONF_BACKUP_PATH not in entry.options + + assert await hass.config_entries.async_setup(entry.entry_id) + + assert entry.data[CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL + assert CONF_SCAN_INTERVAL not in entry.options + assert entry.options[CONF_BACKUP_SHARE] is None + assert entry.options[CONF_BACKUP_PATH] is None diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index 0c7ab6bc1cc..baa91822ca0 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -62,6 +62,7 @@ def dsm_with_photos() -> MagicMock: dsm.photos.get_item_thumbnail_url = AsyncMock( return_value="http://my.thumbnail.url" ) + dsm.file = AsyncMock(get_shared_folders=AsyncMock(return_value=None)) return dsm diff --git a/tests/components/system_bridge/__init__.py b/tests/components/system_bridge/__init__.py index 0606ce8e258..89bd1b652ba 100644 --- a/tests/components/system_bridge/__init__.py +++ b/tests/components/system_bridge/__init__.py @@ -15,9 +15,9 @@ from systembridgemodels.fixtures.modules.processes import FIXTURE_PROCESSES from systembridgemodels.fixtures.modules.system import FIXTURE_SYSTEM from systembridgemodels.modules import Module, ModulesData -from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -44,7 +44,7 @@ FIXTURE_ZEROCONF_INPUT = { CONF_PORT: "9170", } -FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( +FIXTURE_ZEROCONF = ZeroconfServiceInfo( ip_address=ip_address(FIXTURE_USER_INPUT[CONF_HOST]), ip_addresses=[ip_address(FIXTURE_USER_INPUT[CONF_HOST])], port=9170, @@ -62,7 +62,7 @@ FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( }, ) -FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( +FIXTURE_ZEROCONF_BAD = ZeroconfServiceInfo( ip_address=ip_address(FIXTURE_USER_INPUT[CONF_HOST]), ip_addresses=[ip_address(FIXTURE_USER_INPUT[CONF_HOST])], port=9170, diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py index f5cc46da096..60a70da27a2 100644 --- a/tests/components/systemmonitor/test_config_flow.py +++ b/tests/components/systemmonitor/test_config_flow.py @@ -48,17 +48,9 @@ async def test_form_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] is FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" async def test_add_and_remove_processes( diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 6d22c5354a4..a5f5e7623e9 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -5,7 +5,7 @@ import socket from unittest.mock import Mock, patch from freezegun.api import FrozenDateTimeFactory -from psutil._common import sdiskusage, shwtemp, snetio, snicaddr +from psutil._common import sdiskpart, sdiskusage, shwtemp, snetio, snicaddr import pytest from syrupy.assertion import SnapshotAssertion @@ -504,3 +504,43 @@ async def test_remove_obsolete_entities( entity_registry.async_get("sensor.systemmonitor_network_out_veth54321") is not None ) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_no_duplicate_disk_entities( + hass: HomeAssistant, + mock_psutil: Mock, + mock_os: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the sensor.""" + mock_psutil.disk_usage.return_value = sdiskusage( + 500 * 1024**3, 300 * 1024**3, 200 * 1024**3, 60.0 + ) + mock_psutil.disk_partitions.return_value = [ + sdiskpart("test", "/", "ext4", ""), + sdiskpart("test2", "/media/share", "ext4", ""), + sdiskpart("test3", "/incorrect", "", ""), + sdiskpart("test4", "/media/frigate", "ext4", ""), + sdiskpart("test4", "/media/FRIGATE", "ext4", ""), + sdiskpart("hosts", "/etc/hosts", "bind", ""), + sdiskpart("proc", "/proc/run", "proc", ""), + ] + + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + data={}, + options={ + "binary_sensor": {"process": ["python3", "pip"]}, + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + disk_sensor = hass.states.get("sensor.system_monitor_disk_usage_media_frigate") + assert disk_sensor is not None + assert disk_sensor.state == "60.0" + + assert "Platform systemmonitor does not generate unique IDs." not in caplog.text diff --git a/tests/components/tado/snapshots/test_climate.ambr b/tests/components/tado/snapshots/test_climate.ambr new file mode 100644 index 00000000000..6ba35b6f6f2 --- /dev/null +++ b/tests/components/tado/snapshots/test_climate.ambr @@ -0,0 +1,115 @@ +# serializer version: 1 +# name: test_aircon_set_hvac_mode[cool-COOL] + _Call( + tuple( + 3, + 'NEXT_TIME_BLOCK', + 24.76, + None, + 'AIR_CONDITIONING', + 'ON', + 'COOL', + 'AUTO', + None, + None, + None, + None, + ), + dict({ + }), + ) +# --- +# name: test_aircon_set_hvac_mode[dry-DRY] + _Call( + tuple( + 3, + 'NEXT_TIME_BLOCK', + 24.76, + None, + 'AIR_CONDITIONING', + 'ON', + 'DRY', + None, + None, + None, + None, + None, + ), + dict({ + }), + ) +# --- +# name: test_aircon_set_hvac_mode[fan_only-FAN] + _Call( + tuple( + 3, + 'NEXT_TIME_BLOCK', + None, + None, + 'AIR_CONDITIONING', + 'ON', + 'FAN', + None, + None, + None, + None, + None, + ), + dict({ + }), + ) +# --- +# name: test_aircon_set_hvac_mode[heat-HEAT] + _Call( + tuple( + 3, + 'NEXT_TIME_BLOCK', + 24.76, + None, + 'AIR_CONDITIONING', + 'ON', + 'HEAT', + 'AUTO', + None, + None, + None, + None, + ), + dict({ + }), + ) +# --- +# name: test_aircon_set_hvac_mode[off-OFF] + _Call( + tuple( + 3, + 'MANUAL', + None, + None, + 'AIR_CONDITIONING', + 'OFF', + ), + dict({ + }), + ) +# --- +# name: test_heater_set_temperature + _Call( + tuple( + 1, + 'NEXT_TIME_BLOCK', + 22.0, + None, + 'HEATING', + 'ON', + 'HEAT', + None, + None, + None, + None, + None, + ), + dict({ + }), + ) +# --- diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 5a43c728b6e..0699551c9c0 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -1,5 +1,19 @@ """The sensor tests for the tado platform.""" +from unittest.mock import patch + +from PyTado.interface.api.my_tado import TadoZone +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from .util import async_init_integration @@ -121,3 +135,104 @@ async def test_smartac_with_fanlevel_vertical_and_horizontal_swing( # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_heater_set_temperature( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test the set temperature of the heater.""" + + await async_init_integration(hass) + + with ( + patch( + "homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_overlay" + ) as mock_set_state, + patch( + "homeassistant.components.tado.PyTado.interface.api.Tado.get_zone_state", + return_value={"setting": {"temperature": {"celsius": 22.0}}}, + ), + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.baseboard_heater", ATTR_TEMPERATURE: 22.0}, + blocking=True, + ) + + mock_set_state.assert_called_once() + snapshot.assert_match(mock_set_state.call_args) + + +@pytest.mark.parametrize( + ("hvac_mode", "set_hvac_mode"), + [ + (HVACMode.HEAT, "HEAT"), + (HVACMode.DRY, "DRY"), + (HVACMode.FAN_ONLY, "FAN"), + (HVACMode.COOL, "COOL"), + (HVACMode.OFF, "OFF"), + ], +) +async def test_aircon_set_hvac_mode( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + hvac_mode: HVACMode, + set_hvac_mode: str, +) -> None: + """Test the set hvac mode of the air conditioning.""" + + await async_init_integration(hass) + + with ( + patch( + "homeassistant.components.tado.__init__.PyTado.interface.api.Tado.set_zone_overlay" + ) as mock_set_state, + patch( + "homeassistant.components.tado.__init__.PyTado.interface.api.Tado.get_zone_state", + return_value=TadoZone( + zone_id=1, + current_temp=18.7, + connection=None, + current_temp_timestamp="2025-01-02T12:51:52.802Z", + current_humidity=45.1, + current_humidity_timestamp="2025-01-02T12:51:52.802Z", + is_away=False, + current_hvac_action="IDLE", + current_fan_speed=None, + current_fan_level=None, + current_hvac_mode=set_hvac_mode, + current_swing_mode="OFF", + current_vertical_swing_mode="OFF", + current_horizontal_swing_mode="OFF", + target_temp=16.0, + available=True, + power="ON", + link="ONLINE", + ac_power_timestamp=None, + heating_power_timestamp="2025-01-02T13:01:11.758Z", + ac_power=None, + heating_power=None, + heating_power_percentage=0.0, + tado_mode="HOME", + overlay_termination_type="MANUAL", + overlay_termination_timestamp=None, + default_overlay_termination_type="MANUAL", + default_overlay_termination_duration=None, + preparation=False, + open_window=False, + open_window_detected=False, + open_window_attr={}, + precision=0.1, + ), + ), + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.air_conditioning", ATTR_HVAC_MODE: hvac_mode}, + blocking=True, + ) + + mock_set_state.assert_called_once() + snapshot.assert_match(mock_set_state.call_args) diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 63b17dad13e..19acb0aecbd 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -9,7 +9,6 @@ import pytest import requests from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.tado.config_flow import NoHomes from homeassistant.components.tado.const import ( CONF_FALLBACK, @@ -19,16 +18,20 @@ from homeassistant.components.tado.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from tests.common import MockConfigEntry -def _get_mock_tado_api(getMe=None) -> MagicMock: +def _get_mock_tado_api(get_me=None) -> MagicMock: mock_tado = MagicMock() - if isinstance(getMe, Exception): - type(mock_tado).getMe = MagicMock(side_effect=getMe) + if isinstance(get_me, Exception): + type(mock_tado).get_me = MagicMock(side_effect=get_me) else: - type(mock_tado).getMe = MagicMock(return_value=getMe) + type(mock_tado).get_me = MagicMock(return_value=get_me) return mock_tado @@ -61,7 +64,7 @@ async def test_form_exceptions( assert result["errors"] == {"base": error} # Test a retry to recover, upon failure - mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) + mock_tado_api = _get_mock_tado_api(get_me={"homes": [{"id": 1, "name": "myhome"}]}) with ( patch( @@ -131,7 +134,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) + mock_tado_api = _get_mock_tado_api(get_me={"homes": [{"id": 1, "name": "myhome"}]}) with ( patch( @@ -166,7 +169,9 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: response_mock = MagicMock() type(response_mock).status_code = HTTPStatus.UNAUTHORIZED - mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock)) + mock_tado_api = _get_mock_tado_api( + get_me=requests.HTTPError(response=response_mock) + ) with patch( "homeassistant.components.tado.config_flow.Tado", @@ -189,7 +194,9 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: response_mock = MagicMock() type(response_mock).status_code = HTTPStatus.INTERNAL_SERVER_ERROR - mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock)) + mock_tado_api = _get_mock_tado_api( + get_me=requests.HTTPError(response=response_mock) + ) with patch( "homeassistant.components.tado.config_flow.Tado", @@ -210,7 +217,7 @@ async def test_no_homes(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_tado_api = _get_mock_tado_api(getMe={"homes": []}) + mock_tado_api = _get_mock_tado_api(get_me={"homes": []}) with patch( "homeassistant.components.tado.config_flow.Tado", @@ -231,13 +238,13 @@ async def test_form_homekit(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + properties={ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, type="mock_type", ), ) @@ -258,13 +265,13 @@ async def test_form_homekit(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, + properties={ATTR_PROPERTIES_ID: "AA:BB:CC:DD:EE:FF"}, type="mock_type", ), ) @@ -314,7 +321,7 @@ async def test_reconfigure_flow( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) + mock_tado_api = _get_mock_tado_api(get_me={"homes": [{"id": 1, "name": "myhome"}]}) with ( patch( "homeassistant.components.tado.config_flow.Tado", diff --git a/tests/components/tado/test_helper.py b/tests/components/tado/test_helper.py index bdd7977f858..da959c2124a 100644 --- a/tests/components/tado/test_helper.py +++ b/tests/components/tado/test_helper.py @@ -1,45 +1,94 @@ """Helper method tests.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from homeassistant.components.tado import TadoConnector +from PyTado.interface import Tado +import pytest + +from homeassistant.components.tado import TadoDataUpdateCoordinator from homeassistant.components.tado.const import ( CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TIMER, + DOMAIN, ) from homeassistant.components.tado.helper import decide_duration, decide_overlay_mode +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry -def dummy_tado_connector(hass: HomeAssistant, fallback) -> TadoConnector: + +@pytest.fixture +def entry(request: pytest.FixtureRequest) -> MockConfigEntry: + """Fixture for ConfigEntry with optional fallback.""" + fallback = ( + request.param if hasattr(request, "param") else CONST_OVERLAY_TADO_DEFAULT + ) + return MockConfigEntry( + version=1, + minor_version=1, + domain=DOMAIN, + title="Tado", + data={ + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + options={ + "fallback": fallback, + }, + ) + + +@pytest.fixture +def tado() -> Tado: + """Fixture for Tado instance.""" + with patch( + "homeassistant.components.tado.PyTado.interface.api.Tado.set_zone_overlay" + ) as mock_set_zone_overlay: + instance = MagicMock(spec=Tado) + instance.set_zone_overlay = mock_set_zone_overlay + yield instance + + +def dummy_tado_connector( + hass: HomeAssistant, entry: ConfigEntry, tado: Tado +) -> TadoDataUpdateCoordinator: """Return dummy tado connector.""" - return TadoConnector(hass, username="dummy", password="dummy", fallback=fallback) + return TadoDataUpdateCoordinator(hass, entry, tado) -async def test_overlay_mode_duration_set(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("entry", [CONST_OVERLAY_TADO_MODE], indirect=True) +async def test_overlay_mode_duration_set( + hass: HomeAssistant, entry: ConfigEntry, tado: Tado +) -> None: """Test overlay method selection when duration is set.""" - tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_TADO_MODE) - overlay_mode = decide_overlay_mode(tado=tado, duration=3600, zone_id=1) + tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado) + overlay_mode = decide_overlay_mode(coordinator=tado, duration=3600, zone_id=1) # Must select TIMER overlay assert overlay_mode == CONST_OVERLAY_TIMER -async def test_overlay_mode_next_time_block_fallback(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("entry", [CONST_OVERLAY_TADO_MODE], indirect=True) +async def test_overlay_mode_next_time_block_fallback( + hass: HomeAssistant, entry: ConfigEntry, tado: Tado +) -> None: """Test overlay method selection when duration is not set.""" - integration_fallback = CONST_OVERLAY_TADO_MODE - tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) - overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=1) + tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado) + overlay_mode = decide_overlay_mode(coordinator=tado, duration=None, zone_id=1) # Must fallback to integration wide setting - assert overlay_mode == integration_fallback + assert overlay_mode == CONST_OVERLAY_TADO_MODE -async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("entry", [CONST_OVERLAY_TADO_DEFAULT], indirect=True) +async def test_overlay_mode_tado_default_fallback( + hass: HomeAssistant, entry: ConfigEntry, tado: Tado +) -> None: """Test overlay method selection when tado default is selected.""" - integration_fallback = CONST_OVERLAY_TADO_DEFAULT zone_fallback = CONST_OVERLAY_MANUAL - tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) + tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado) class MockZoneData: def __init__(self) -> None: @@ -49,28 +98,40 @@ async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None: zone_data = {"zone": {zone_id: MockZoneData()}} with patch.dict(tado.data, zone_data): - overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=zone_id) + overlay_mode = decide_overlay_mode( + coordinator=tado, duration=None, zone_id=zone_id + ) # Must fallback to zone setting assert overlay_mode == zone_fallback -async def test_duration_enabled_without_tado_default(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("entry", [CONST_OVERLAY_MANUAL], indirect=True) +async def test_duration_enabled_without_tado_default( + hass: HomeAssistant, entry: ConfigEntry, tado: Tado +) -> None: """Test duration decide method when overlay is timer and duration is set.""" overlay = CONST_OVERLAY_TIMER expected_duration = 600 - tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_MANUAL) + tado = dummy_tado_connector(hass=hass, entry=entry, tado=tado) duration = decide_duration( - tado=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0 + coordinator=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0 ) # Should return the same duration value assert duration == expected_duration -async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("entry", [CONST_OVERLAY_TIMER], indirect=True) +async def test_duration_enabled_with_tado_default( + hass: HomeAssistant, entry: ConfigEntry, tado: Tado +) -> None: """Test overlay method selection when ended up with timer overlay and None duration.""" zone_fallback = CONST_OVERLAY_TIMER expected_duration = 45000 - tado = dummy_tado_connector(hass=hass, fallback=zone_fallback) + tado = dummy_tado_connector( + hass=hass, + entry=entry, + tado=tado, + ) class MockZoneData: def __init__(self) -> None: @@ -81,7 +142,7 @@ async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None: zone_data = {"zone": {zone_id: MockZoneData()}} with patch.dict(tado.data, zone_data): duration = decide_duration( - tado=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback + coordinator=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback ) # Must fallback to zone timer setting assert duration == expected_duration diff --git a/tests/components/tado/test_service.py b/tests/components/tado/test_service.py index 994f135199f..336bef55ea1 100644 --- a/tests/components/tado/test_service.py +++ b/tests/components/tado/test_service.py @@ -40,7 +40,7 @@ async def test_add_meter_readings( config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] fixture: str = load_fixture("tado/add_readings_success.json") with patch( - "PyTado.interface.Tado.set_eiq_meter_readings", + "PyTado.interface.api.Tado.set_eiq_meter_readings", return_value=json.loads(fixture), ): response: None = await hass.services.async_call( @@ -65,7 +65,7 @@ async def test_add_meter_readings_exception( config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] with ( patch( - "PyTado.interface.Tado.set_eiq_meter_readings", + "PyTado.interface.api.Tado.set_eiq_meter_readings", side_effect=RequestException("Error"), ), pytest.raises(HomeAssistantError) as exc, @@ -80,7 +80,7 @@ async def test_add_meter_readings_exception( blocking=True, ) - assert "Could not set meter reading" in str(exc) + assert "Error setting Tado meter reading: Error" in str(exc.value) async def test_add_meter_readings_invalid( @@ -94,7 +94,7 @@ async def test_add_meter_readings_invalid( fixture: str = load_fixture("tado/add_readings_invalid_meter_reading.json") with ( patch( - "PyTado.interface.Tado.set_eiq_meter_readings", + "PyTado.interface.api.Tado.set_eiq_meter_readings", return_value=json.loads(fixture), ), pytest.raises(HomeAssistantError) as exc, @@ -123,7 +123,7 @@ async def test_add_meter_readings_duplicate( fixture: str = load_fixture("tado/add_readings_duplicated_meter_reading.json") with ( patch( - "PyTado.interface.Tado.set_eiq_meter_readings", + "PyTado.interface.api.Tado.set_eiq_meter_readings", return_value=json.loads(fixture), ), pytest.raises(HomeAssistantError) as exc, diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index a76858ab98e..5bf87dbed33 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -188,3 +188,8 @@ async def async_init_integration( if not skip_setup: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + + # For a first refresh + await entry.runtime_data.coordinator.async_refresh() + await entry.runtime_data.mobile_coordinator.async_refresh() + await hass.async_block_till_done() diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index 064b391c43a..d04f2e726b5 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -20,6 +20,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -50,6 +51,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -129,6 +132,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 17b656ec5fd..7d3d10aa609 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -20,6 +20,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -50,6 +51,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index b69bd9e6410..1a26a6c98a7 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -21,6 +21,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -51,6 +52,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -101,6 +103,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -131,6 +134,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index 3e2e0577ad5..7b906ef1976 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -29,6 +29,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( diff --git a/tests/components/tailwind/test_config_flow.py b/tests/components/tailwind/test_config_flow.py index ca6fbacf0fc..2e8a8e7a727 100644 --- a/tests/components/tailwind/test_config_flow.py +++ b/tests/components/tailwind/test_config_flow.py @@ -11,13 +11,13 @@ from gotailwind import ( import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import zeroconf -from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.tailwind.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -156,7 +156,7 @@ async def test_zeroconf_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], port=80, @@ -208,7 +208,7 @@ async def test_zeroconf_flow_abort_incompatible_properties( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], port=80, @@ -243,7 +243,7 @@ async def test_zeroconf_flow_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], port=80, @@ -303,7 +303,7 @@ async def test_zeroconf_flow_not_discovered_again( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], port=80, diff --git a/tests/components/tankerkoenig/test_coordinator.py b/tests/components/tankerkoenig/test_coordinator.py index 3ba0dc31c5f..ff2ceca7442 100644 --- a/tests/components/tankerkoenig/test_coordinator.py +++ b/tests/components/tankerkoenig/test_coordinator.py @@ -25,7 +25,7 @@ from homeassistant.const import ATTR_ID, CONF_SHOW_ON_MAP, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .const import CONFIG_DATA diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index be011e595b9..8a5a78cd366 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -24,6 +24,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -103,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -149,6 +151,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -254,6 +257,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -401,6 +405,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -452,6 +457,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -503,6 +509,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -586,6 +593,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -632,6 +640,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -741,6 +750,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -824,6 +834,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -875,6 +886,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -990,6 +1002,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1041,6 +1054,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1156,6 +1170,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1239,6 +1254,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1290,6 +1306,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1405,6 +1422,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1552,6 +1570,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1603,6 +1622,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1654,6 +1674,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1732,6 +1753,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1860,6 +1882,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1909,6 +1932,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1958,6 +1982,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 5abb9ab9bf2..ff951e058cb 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -13,6 +13,7 @@ from hatasmota.utils import ( ) import pytest +from homeassistant import core as ha from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -22,9 +23,8 @@ from homeassistant.const import ( STATE_UNKNOWN, Platform, ) -import homeassistant.core as ha from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .test_common import ( DEFAULT_CONFIG, diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 4d2c821fff4..674ae316ecc 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -27,7 +27,7 @@ from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import async_fire_mqtt_message +from tests.common import MockMqttReasonCode, async_fire_mqtt_message from tests.typing import MqttMockHAClient, MqttMockPahoClient, WebSocketGenerator DEFAULT_CONFIG = { @@ -165,7 +165,7 @@ async def help_test_availability_when_connection_lost( # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -174,7 +174,7 @@ async def help_test_availability_when_connection_lost( # Reconnected to MQTT server -> state still unavailable mqtt_mock.connected = True - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -226,7 +226,7 @@ async def help_test_deep_sleep_availability_when_connection_lost( # Disconnected from MQTT server -> state changed to unavailable mqtt_mock.connected = False - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -235,7 +235,7 @@ async def help_test_deep_sleep_availability_when_connection_lost( # Reconnected to MQTT server -> state no longer unavailable mqtt_mock.connected = True - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -478,7 +478,7 @@ async def help_test_availability_poll_state( # Disconnected from MQTT server mqtt_mock.connected = False - mqtt_client_mock.on_disconnect(None, None, 0) + mqtt_client_mock.on_disconnect(None, None, 0, MockMqttReasonCode()) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() @@ -486,7 +486,7 @@ async def help_test_availability_poll_state( # Reconnected to MQTT server mqtt_mock.connected = True - mqtt_client_mock.on_connect(None, None, None, 0) + mqtt_client_mock.on_connect(None, None, None, MockMqttReasonCode()) await hass.async_block_till_done() await hass.async_block_till_done() await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 4f4daee1301..6a2b7699840 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -1514,7 +1514,7 @@ async def _test_split_light( await common.async_turn_on(hass, entity) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Power{idx+num_switches+1} ON", + f"NoDelay;Power{idx + num_switches + 1} ON", 0, False, ) @@ -1524,7 +1524,7 @@ async def _test_split_light( await common.async_turn_on(hass, entity, brightness=(idx + 1) * 25.5) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Channel{idx+num_switches+1} {(idx+1)*10}", + f"NoDelay;Channel{idx + num_switches + 1} {(idx + 1) * 10}", 0, False, ) @@ -1595,7 +1595,7 @@ async def _test_unlinked_light( await common.async_turn_on(hass, entity) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Power{idx+num_switches+1} ON", + f"NoDelay;Power{idx + num_switches + 1} ON", 0, False, ) @@ -1605,7 +1605,7 @@ async def _test_unlinked_light( await common.async_turn_on(hass, entity, brightness=(idx + 1) * 25.5) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Dimmer{idx+1} {(idx+1)*10}", + f"NoDelay;Dimmer{idx + 1} {(idx + 1) * 10}", 0, False, ) diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index 27003df46cd..ade4b9f93d4 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import call, patch import pytest -import homeassistant.components.tcp.common as tcp +from homeassistant.components.tcp import common as tcp from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/technove/snapshots/test_binary_sensor.ambr b/tests/components/technove/snapshots/test_binary_sensor.ambr index cc2dcf4a04a..5d9bcd2175a 100644 --- a/tests/components/technove/snapshots/test_binary_sensor.ambr +++ b/tests/components/technove/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -45,53 +46,6 @@ 'state': 'off', }) # --- -# name: test_sensors[binary_sensor.technove_station_charging-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.technove_station_charging', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Charging', - 'platform': 'technove', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'AA:AA:AA:AA:AA:BB_is_session_active', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[binary_sensor.technove_station_charging-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery_charging', - 'friendly_name': 'TechnoVE Station Charging', - }), - 'context': , - 'entity_id': 'binary_sensor.technove_station_charging', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_sensors[binary_sensor.technove_station_conflict_with_power_sharing_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -99,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -145,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -191,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -237,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/technove/snapshots/test_number.ambr b/tests/components/technove/snapshots/test_number.ambr index 622c04d542a..eea4b0cb64c 100644 --- a/tests/components/technove/snapshots/test_number.ambr +++ b/tests/components/technove/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/technove/snapshots/test_sensor.ambr b/tests/components/technove/snapshots/test_sensor.ambr index 149155519d4..dec671b0f34 100644 --- a/tests/components/technove/snapshots/test_sensor.ambr +++ b/tests/components/technove/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +164,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -212,6 +216,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -263,6 +268,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -320,6 +326,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -376,6 +383,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -425,6 +433,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/technove/snapshots/test_switch.ambr b/tests/components/technove/snapshots/test_switch.ambr index 6febc8c768c..a5f8411747b 100644 --- a/tests/components/technove/snapshots/test_switch.ambr +++ b/tests/components/technove/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/technove/test_binary_sensor.py b/tests/components/technove/test_binary_sensor.py index 0a90093779e..93d4805cecb 100644 --- a/tests/components/technove/test_binary_sensor.py +++ b/tests/components/technove/test_binary_sensor.py @@ -45,7 +45,6 @@ async def test_sensors( "entity_id", [ "binary_sensor.technove_station_static_ip", - "binary_sensor.technove_station_charging", ], ) @pytest.mark.usefixtures("init_integration") diff --git a/tests/components/technove/test_config_flow.py b/tests/components/technove/test_config_flow.py index 81e0b32b55b..99a8e231f73 100644 --- a/tests/components/technove/test_config_flow.py +++ b/tests/components/technove/test_config_flow.py @@ -6,12 +6,12 @@ from unittest.mock import AsyncMock, MagicMock import pytest from technove import TechnoVEConnectionError -from homeassistant.components import zeroconf from homeassistant.components.technove.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -112,7 +112,7 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -153,7 +153,7 @@ async def test_zeroconf_during_onboarding( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -184,7 +184,7 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -225,7 +225,7 @@ async def test_zeroconf_without_mac_station_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -250,7 +250,7 @@ async def test_zeroconf_with_mac_station_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", diff --git a/tests/components/tedee/snapshots/test_binary_sensor.ambr b/tests/components/tedee/snapshots/test_binary_sensor.ambr index e3238dacda1..c2210a7ca5d 100644 --- a/tests/components/tedee/snapshots/test_binary_sensor.ambr +++ b/tests/components/tedee/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -146,6 +149,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -192,6 +196,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -239,6 +244,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -286,6 +292,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -332,6 +339,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index af559f561b2..28b5ef7a7ed 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index cca988663d2..432c3ebd19f 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -20,6 +20,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -50,6 +51,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -85,6 +87,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -132,6 +135,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tedee/snapshots/test_sensor.ambr b/tests/components/tedee/snapshots/test_sensor.ambr index 297fe9b0d37..22679c4153a 100644 --- a/tests/components/tedee/snapshots/test_sensor.ambr +++ b/tests/components/tedee/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +164,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/telegram_bot/conftest.py b/tests/components/telegram_bot/conftest.py index 93137c3815e..f15db7eba2b 100644 --- a/tests/components/telegram_bot/conftest.py +++ b/tests/components/telegram_bot/conftest.py @@ -105,6 +105,14 @@ def mock_external_calls() -> Generator[None]: patch.object(BotMock, "get_me", return_value=test_user), patch.object(BotMock, "bot", test_user), patch.object(BotMock, "send_message", return_value=message), + patch.object(BotMock, "send_photo", return_value=message), + patch.object(BotMock, "send_sticker", return_value=message), + patch.object(BotMock, "send_video", return_value=message), + patch.object(BotMock, "send_document", return_value=message), + patch.object(BotMock, "send_voice", return_value=message), + patch.object(BotMock, "send_animation", return_value=message), + patch.object(BotMock, "send_location", return_value=message), + patch.object(BotMock, "send_poll", return_value=message), patch("telegram.ext.Updater._bootstrap"), ): yield diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index bdf6ba72fcc..c9038003cfc 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -1,17 +1,33 @@ """Tests for the telegram_bot component.""" +import base64 +import io from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, mock_open, patch import pytest from telegram import Update from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut from homeassistant.components.telegram_bot import ( + ATTR_FILE, + ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_MESSAGE, ATTR_MESSAGE_THREAD_ID, + ATTR_OPTIONS, + ATTR_QUESTION, + ATTR_STICKER_ID, DOMAIN, + SERVICE_SEND_ANIMATION, + SERVICE_SEND_DOCUMENT, + SERVICE_SEND_LOCATION, SERVICE_SEND_MESSAGE, + SERVICE_SEND_PHOTO, + SERVICE_SEND_POLL, + SERVICE_SEND_STICKER, + SERVICE_SEND_VIDEO, + SERVICE_SEND_VOICE, ) from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL from homeassistant.const import EVENT_HOMEASSISTANT_START @@ -32,23 +48,125 @@ async def test_polling_platform_init(hass: HomeAssistant, polling_platform) -> N assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True -async def test_send_message(hass: HomeAssistant, webhook_platform) -> None: - """Test the send_message service.""" +@pytest.mark.parametrize( + ("service", "input"), + [ + ( + SERVICE_SEND_MESSAGE, + {ATTR_MESSAGE: "test_message", ATTR_MESSAGE_THREAD_ID: "123"}, + ), + ( + SERVICE_SEND_STICKER, + { + ATTR_STICKER_ID: "1", + ATTR_MESSAGE_THREAD_ID: "123", + }, + ), + ( + SERVICE_SEND_POLL, + { + ATTR_QUESTION: "Question", + ATTR_OPTIONS: ["Yes", "No"], + }, + ), + ( + SERVICE_SEND_LOCATION, + { + ATTR_MESSAGE: "test_message", + ATTR_MESSAGE_THREAD_ID: "123", + ATTR_LONGITUDE: "1.123", + ATTR_LATITUDE: "1.123", + }, + ), + ], +) +async def test_send_message( + hass: HomeAssistant, webhook_platform, service: str, input: dict[str] +) -> None: + """Test the send_message service. Tests any service that does not require files to be sent.""" context = Context() events = async_capture_events(hass, "telegram_sent") - await hass.services.async_call( + response = await hass.services.async_call( DOMAIN, - SERVICE_SEND_MESSAGE, - {ATTR_MESSAGE: "test_message", ATTR_MESSAGE_THREAD_ID: "123"}, + service, + input, blocking=True, context=context, + return_response=True, ) await hass.async_block_till_done() assert len(events) == 1 assert events[0].context == context + assert len(response["chats"]) == 1 + assert (response["chats"][0]["message_id"]) == 12345 + + +@patch( + "builtins.open", + mock_open( + read_data=base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=" + ) + ), + create=True, +) +def _read_file_as_bytesio_mock(file_path): + """Convert file to BytesIO for testing.""" + _file = None + + with open(file_path, encoding="utf8") as file_handler: + _file = io.BytesIO(file_handler.read()) + + _file.name = "dummy" + _file.seek(0) + + return _file + + +@pytest.mark.parametrize( + "service", + [ + SERVICE_SEND_PHOTO, + SERVICE_SEND_ANIMATION, + SERVICE_SEND_VIDEO, + SERVICE_SEND_VOICE, + SERVICE_SEND_DOCUMENT, + ], +) +async def test_send_file(hass: HomeAssistant, webhook_platform, service: str) -> None: + """Test the send_file service (photo, animation, video, document...).""" + context = Context() + events = async_capture_events(hass, "telegram_sent") + + hass.config.allowlist_external_dirs.add("/media/") + + # Mock the file handler read with our base64 encoded dummy file + with patch( + "homeassistant.components.telegram_bot._read_file_as_bytesio", + _read_file_as_bytesio_mock, + ): + response = await hass.services.async_call( + DOMAIN, + service, + { + ATTR_FILE: "/media/dummy", + ATTR_MESSAGE_THREAD_ID: "123", + }, + blocking=True, + context=context, + return_response=True, + ) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].context == context + + assert len(response["chats"]) == 1 + assert (response["chats"][0]["message_id"]) == 12345 + async def test_send_message_thread(hass: HomeAssistant, webhook_platform) -> None: """Test the send_message service for threads.""" @@ -66,7 +184,7 @@ async def test_send_message_thread(hass: HomeAssistant, webhook_platform) -> Non assert len(events) == 1 assert events[0].context == context - assert events[0].data[ATTR_MESSAGE_THREAD_ID] == "123" + assert events[0].data[ATTR_MESSAGE_THREAD_ID] == 123 async def test_webhook_endpoint_generates_telegram_text_event( diff --git a/tests/components/temper/test_sensor.py b/tests/components/temper/test_sensor.py index d1e74f1ab0f..445adc0b5bd 100644 --- a/tests/components/temper/test_sensor.py +++ b/tests/components/temper/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import Mock, patch from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 3e3a629b4be..a7ee953bb09 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -23,7 +23,7 @@ from homeassistant.core import Context, CoreState, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/template/test_blueprint.py b/tests/components/template/test_blueprint.py index 1df9e738b06..dd008a27822 100644 --- a/tests/components/template/test_blueprint.py +++ b/tests/components/template/test_blueprint.py @@ -19,7 +19,7 @@ from homeassistant.components.template import DOMAIN, SERVICE_RELOAD from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from homeassistant.util import yaml +from homeassistant.util import yaml as yaml_util from tests.common import async_mock_service @@ -40,7 +40,7 @@ def patch_blueprint( return orig_load(self, path) return Blueprint( - yaml.load_yaml(data_path), + yaml_util.load_yaml(data_path), expected_domain=self.domain, path=path, schema=BLUEPRINT_SCHEMA, @@ -149,6 +149,69 @@ async def test_inverted_binary_sensor( ) +async def test_reload_template_when_blueprint_changes(hass: HomeAssistant) -> None: + """Test a template is updated at reload if the blueprint has changed.""" + hass.states.async_set("binary_sensor.foo", "on", {"friendly_name": "Foo"}) + config = { + DOMAIN: [ + { + "use_blueprint": { + "path": "inverted_binary_sensor.yaml", + "input": {"reference_entity": "binary_sensor.foo"}, + }, + "name": "Inverted foo", + }, + ] + } + with patch_blueprint( + "inverted_binary_sensor.yaml", + BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml", + ): + assert await async_setup_component(hass, DOMAIN, config) + + hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"}) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.foo").state == "off" + + inverted = hass.states.get("binary_sensor.inverted_foo") + assert inverted + assert inverted.state == "on" + + # Reload the automations without any change, but with updated blueprint + blueprint_config = yaml_util.load_yaml( + BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml" + ) + blueprint_config["binary_sensor"]["state"] = "{{ states(reference_entity) }}" + with ( + patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ), + patch( + "homeassistant.components.blueprint.models.yaml_util.load_yaml_dict", + autospec=True, + return_value=blueprint_config, + ), + ): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + + hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"}) + await hass.async_block_till_done() + + not_inverted = hass.states.get("binary_sensor.inverted_foo") + assert not_inverted + assert not_inverted.state == "off" + + hass.states.async_set("binary_sensor.foo", "on", {"friendly_name": "Foo"}) + await hass.async_block_till_done() + + not_inverted = hass.states.get("binary_sensor.inverted_foo") + assert not_inverted + assert not_inverted.state == "on" + + async def test_domain_blueprint(hass: HomeAssistant) -> None: """Test DomainBlueprint services.""" reload_handler_calls = async_mock_service(hass, DOMAIN, SERVICE_RELOAD) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 929a890ab38..6f0e6be8a2a 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -24,11 +24,11 @@ from homeassistant.const import ( from homeassistant.core import Context, CoreState, HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import ATTR_COMPONENT, async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, @@ -393,7 +393,7 @@ async def test_creating_sensor_loads_group(hass: HomeAssistant) -> None: async def async_setup_template( hass: HomeAssistant, config: ConfigType, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: order.append("sensor.template") diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index a131f5f606b..49b89b61d34 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, ServiceCall, callback from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, mock_component diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index 0dc5d87984f..06d2b54c936 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -15,6 +15,7 @@ from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES from .const import ( COMMAND_OK, + ENERGY_HISTORY, LIVE_STATUS, PRODUCTS, SITE_INFO, @@ -33,7 +34,9 @@ def mock_expires_at() -> int: return time.time() + 3600 -def create_config_entry(expires_at: int, scopes: list[Scope]) -> MockConfigEntry: +def create_config_entry( + expires_at: int, scopes: list[Scope], implementation: str = DOMAIN +) -> MockConfigEntry: """Create Tesla Fleet entry in Home Assistant.""" access_token = jwt.encode( { @@ -51,7 +54,7 @@ def create_config_entry(expires_at: int, scopes: list[Scope]) -> MockConfigEntry title=UID, unique_id=UID, data={ - "auth_implementation": DOMAIN, + "auth_implementation": implementation, "token": { "status": 0, "userid": UID, @@ -90,6 +93,12 @@ def readonly_config_entry(expires_at: int) -> MockConfigEntry: ) +@pytest.fixture +def bad_config_entry(expires_at: int) -> MockConfigEntry: + """Create Tesla Fleet entry in Home Assistant.""" + return create_config_entry(expires_at, SCOPES, "bad") + + @pytest.fixture(autouse=True) def mock_products() -> Generator[AsyncMock]: """Mock Tesla Fleet Api products method.""" @@ -169,6 +178,16 @@ def mock_request(): yield mock_request +@pytest.fixture(autouse=True) +def mock_energy_history(): + """Mock Teslemetry Energy Specific site_info method.""" + with patch( + "homeassistant.components.teslemetry.EnergySpecific.energy_history", + return_value=ENERGY_HISTORY, + ) as mock_live_status: + yield mock_live_status + + @pytest.fixture(autouse=True) def mock_signed_command() -> Generator[AsyncMock]: """Mock Tesla Fleet Api signed_command method.""" diff --git a/tests/components/tesla_fleet/const.py b/tests/components/tesla_fleet/const.py index 76b4ae20092..d584e7b93d5 100644 --- a/tests/components/tesla_fleet/const.py +++ b/tests/components/tesla_fleet/const.py @@ -11,6 +11,7 @@ PRODUCTS = load_json_object_fixture("products.json", DOMAIN) VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN) VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN) LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) +ENERGY_HISTORY = load_json_object_fixture("energy_history.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) COMMAND_OK = {"response": {"result": True, "reason": ""}} diff --git a/tests/components/tesla_fleet/fixtures/energy_history.json b/tests/components/tesla_fleet/fixtures/energy_history.json new file mode 100644 index 00000000000..befe12cc903 --- /dev/null +++ b/tests/components/tesla_fleet/fixtures/energy_history.json @@ -0,0 +1,45 @@ +{ + "response": { + "period": "day", + "time_series": [ + { + "timestamp": "2023-06-01T01:00:00-07:00", + "solar_energy_exported": 70940, + "generator_energy_exported": 0, + "grid_energy_imported": 521, + "grid_services_energy_imported": 17.53125, + "grid_services_energy_exported": 3.80859375, + "grid_energy_exported_from_solar": 43660, + "grid_energy_exported_from_generator": 0, + "grid_energy_exported_from_battery": 19, + "battery_energy_exported": 10030, + "battery_energy_imported_from_grid": 80, + "battery_energy_imported_from_solar": 16800, + "battery_energy_imported_from_generator": 0, + "consumer_energy_imported_from_grid": 441, + "consumer_energy_imported_from_solar": 10480, + "consumer_energy_imported_from_battery": 10011, + "consumer_energy_imported_from_generator": 0 + }, + { + "timestamp": "2023-06-01T01:05:00-07:00", + "solar_energy_exported": 140940, + "generator_energy_exported": 1, + "grid_energy_imported": 1021, + "grid_services_energy_imported": 27.53125, + "grid_services_energy_exported": 6.80859375, + "grid_energy_exported_from_solar": 83660, + "grid_energy_exported_from_generator": 0, + "grid_energy_exported_from_battery": 29, + "battery_energy_exported": 20030, + "battery_energy_imported_from_grid": 0, + "battery_energy_imported_from_solar": 26800, + "battery_energy_imported_from_generator": 0, + "consumer_energy_imported_from_grid": 841, + "consumer_energy_imported_from_solar": 20480, + "consumer_energy_imported_from_battery": 20011, + "consumer_energy_imported_from_generator": 0 + } + ] + } +} diff --git a/tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr b/tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr similarity index 98% rename from tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr rename to tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr index 479d647e1c7..4e34f586280 100644 --- a/tests/components/tesla_fleet/snapshots/test_binary_sensors.ambr +++ b/tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -237,6 +242,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -284,6 +290,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -331,6 +338,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -377,6 +385,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -424,6 +433,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -471,6 +481,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -518,6 +529,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -565,6 +577,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -612,6 +625,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -658,6 +672,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -704,6 +719,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -751,6 +767,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -798,6 +815,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -845,6 +863,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -892,6 +911,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -938,6 +958,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -985,6 +1006,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1032,6 +1054,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1079,6 +1102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1126,6 +1150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1173,6 +1198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1219,6 +1245,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_button.ambr b/tests/components/tesla_fleet/snapshots/test_button.ambr index 8b5270d4852..145b10112b3 100644 --- a/tests/components/tesla_fleet/snapshots/test_button.ambr +++ b/tests/components/tesla_fleet/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_climate.ambr b/tests/components/tesla_fleet/snapshots/test_climate.ambr index 696f8c37f08..f3b36730c3f 100644 --- a/tests/components/tesla_fleet/snapshots/test_climate.ambr +++ b/tests/components/tesla_fleet/snapshots/test_climate.ambr @@ -15,6 +15,7 @@ 'target_temp_step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -85,6 +86,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -156,6 +158,7 @@ 'target_temp_step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -225,6 +228,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -296,6 +300,7 @@ 'target_temp_step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -365,6 +370,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_cover.ambr b/tests/components/tesla_fleet/snapshots/test_cover.ambr index dbdb003d802..ed6969262f1 100644 --- a/tests/components/tesla_fleet/snapshots/test_cover.ambr +++ b/tests/components/tesla_fleet/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -246,6 +251,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +300,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -342,6 +349,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -390,6 +398,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -438,6 +447,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -486,6 +496,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -534,6 +545,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -582,6 +594,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -630,6 +643,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -678,6 +692,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr index 02ad4b01002..dc142c4ffeb 100644 --- a/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr +++ b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_init.ambr b/tests/components/tesla_fleet/snapshots/test_init.ambr index e9828db9f1b..c482d33de86 100644 --- a/tests/components/tesla_fleet/snapshots/test_init.ambr +++ b/tests/components/tesla_fleet/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -67,6 +69,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -99,6 +102,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/tesla_fleet/snapshots/test_lock.ambr b/tests/components/tesla_fleet/snapshots/test_lock.ambr index 3384bb0eb97..e98ad09caad 100644 --- a/tests/components/tesla_fleet/snapshots/test_lock.ambr +++ b/tests/components/tesla_fleet/snapshots/test_lock.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_media_player.ambr b/tests/components/tesla_fleet/snapshots/test_media_player.ambr index cc3018364a5..77c46faedd7 100644 --- a/tests/components/tesla_fleet/snapshots/test_media_player.ambr +++ b/tests/components/tesla_fleet/snapshots/test_media_player.ambr @@ -7,6 +7,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -85,6 +86,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_number.ambr b/tests/components/tesla_fleet/snapshots/test_number.ambr index 00dd67015fe..1981544a024 100644 --- a/tests/components/tesla_fleet/snapshots/test_number.ambr +++ b/tests/components/tesla_fleet/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -127,6 +129,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -184,6 +187,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_select.ambr b/tests/components/tesla_fleet/snapshots/test_select.ambr index f29ce841113..171b52decf1 100644 --- a/tests/components/tesla_fleet/snapshots/test_select.ambr +++ b/tests/components/tesla_fleet/snapshots/test_select.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -127,6 +129,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -186,6 +189,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -245,6 +249,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -304,6 +309,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -363,6 +369,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -422,6 +429,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -481,6 +489,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -539,6 +548,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr index 2c3780749ca..f7349c9e2d8 100644 --- a/tests/components/tesla_fleet/snapshots/test_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -1,4 +1,448 @@ # serializer version: 1 +# name: test_sensors[sensor.energy_site_battery_charged-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery charged', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_battery_charge', + 'unique_id': '123456-total_battery_charge', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_charged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery charged', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_charged-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery charged', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_discharged-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery discharged', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_battery_discharge', + 'unique_id': '123456-total_battery_discharge', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_discharged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery discharged', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_discharged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_discharged-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery discharged', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_discharged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery exported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_energy_exported', + 'unique_id': '123456-battery_energy_exported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.06', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_exported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.06', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_generator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_imported_from_generator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery imported from generator', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_energy_imported_from_generator', + 'unique_id': '123456-battery_energy_imported_from_generator', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_generator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery imported from generator', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_imported_from_generator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_generator-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery imported from generator', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_imported_from_generator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_imported_from_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery imported from grid', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_energy_imported_from_grid', + 'unique_id': '123456-battery_energy_imported_from_grid', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery imported from grid', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_imported_from_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.08', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_grid-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery imported from grid', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_imported_from_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.08', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_solar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_imported_from_solar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery imported from solar', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_energy_imported_from_solar', + 'unique_id': '123456-battery_energy_imported_from_solar', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_solar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery imported from solar', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_imported_from_solar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '43.6', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_imported_from_solar-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Battery imported from solar', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_imported_from_solar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '43.6', + }) +# --- # name: test_sensors[sensor.energy_site_battery_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -8,6 +452,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -72,6 +517,302 @@ 'state': '5.06', }) # --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_consumer_imported_from_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumer imported from battery', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumer_energy_imported_from_battery', + 'unique_id': '123456-consumer_energy_imported_from_battery', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.022', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_battery-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.022', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_generator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_consumer_imported_from_generator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumer imported from generator', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumer_energy_imported_from_generator', + 'unique_id': '123456-consumer_energy_imported_from_generator', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_generator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from generator', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_generator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_generator-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from generator', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_generator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_consumer_imported_from_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumer imported from grid', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumer_energy_imported_from_grid', + 'unique_id': '123456-consumer_energy_imported_from_grid', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from grid', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.282', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_grid-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from grid', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.282', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_solar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_consumer_imported_from_solar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumer imported from solar', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'consumer_energy_imported_from_solar', + 'unique_id': '123456-consumer_energy_imported_from_solar', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_solar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from solar', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_solar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.96', + }) +# --- +# name: test_sensors[sensor.energy_site_consumer_imported_from_solar-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Consumer imported from solar', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_consumer_imported_from_solar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.96', + }) +# --- # name: test_sensors[sensor.energy_site_energy_left-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -81,6 +822,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -145,6 +887,80 @@ 'state': '38.8964736842105', }) # --- +# name: test_sensors[sensor.energy_site_generator_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_generator_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Generator exported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'generator_energy_exported', + 'unique_id': '123456-generator_energy_exported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_generator_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Generator exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.001', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_exported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Generator exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.001', + }) +# --- # name: test_sensors[sensor.energy_site_generator_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -154,6 +970,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -218,6 +1035,376 @@ 'state': '0.0', }) # --- +# name: test_sensors[sensor.energy_site_grid_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid exported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_grid_energy_exported', + 'unique_id': '123456-total_grid_energy_exported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_exported_from_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid exported from battery', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_energy_exported_from_battery', + 'unique_id': '123456-grid_energy_exported_from_battery', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported from battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported_from_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.048', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_battery-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported from battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported_from_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.048', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_generator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_exported_from_generator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid exported from generator', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_energy_exported_from_generator', + 'unique_id': '123456-grid_energy_exported_from_generator', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_generator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported from generator', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported_from_generator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_generator-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported from generator', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported_from_generator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_solar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_exported_from_solar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid exported from solar', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_energy_exported_from_solar', + 'unique_id': '123456-grid_energy_exported_from_solar', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_solar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported from solar', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported_from_solar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '127.32', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_exported_from_solar-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid exported from solar', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_exported_from_solar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '127.32', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_imported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_imported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid imported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_energy_imported', + 'unique_id': '123456-grid_energy_imported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_imported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid imported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_imported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.542', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_imported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid imported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_imported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.542', + }) +# --- # name: test_sensors[sensor.energy_site_grid_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -227,6 +1414,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -291,6 +1479,154 @@ 'state': '0.0', }) # --- +# name: test_sensors[sensor.energy_site_grid_services_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_services_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid services exported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_energy_exported', + 'unique_id': '123456-grid_services_energy_exported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid services exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0106171875', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_exported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid services exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0106171875', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_imported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_services_imported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid services imported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_energy_imported', + 'unique_id': '123456-grid_services_energy_imported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_imported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid services imported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_imported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0450625', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_imported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Grid services imported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_imported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0450625', + }) +# --- # name: test_sensors[sensor.energy_site_grid_services_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -300,6 +1636,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -379,6 +1716,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -447,6 +1785,80 @@ 'state': 'on_grid', }) # --- +# name: test_sensors[sensor.energy_site_home_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_home_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Home usage', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_home_usage', + 'unique_id': '123456-total_home_usage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_home_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Home usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_home_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_home_usage-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Home usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_home_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_sensors[sensor.energy_site_load_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -456,6 +1868,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -529,6 +1942,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -590,6 +2004,154 @@ 'state': '95.5053740373966', }) # --- +# name: test_sensors[sensor.energy_site_solar_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_solar_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar exported', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_energy_exported', + 'unique_id': '123456-solar_energy_exported', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_solar_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Solar exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '211.88', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_exported-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Solar exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '211.88', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_generated-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_solar_generated', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar generated', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_solar_generation', + 'unique_id': '123456-total_solar_generation', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_solar_generated-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Solar generated', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_generated', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_generated-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Energy Site Solar generated', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_generated', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_sensors[sensor.energy_site_solar_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -599,6 +2161,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -672,6 +2235,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -743,6 +2307,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -802,6 +2367,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -867,6 +2433,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -934,6 +2501,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1005,6 +2573,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1066,6 +2635,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1136,6 +2706,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1206,6 +2777,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1273,6 +2845,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1340,6 +2913,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1414,6 +2988,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1493,6 +3068,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1563,6 +3139,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1633,6 +3210,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1704,6 +3282,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1765,6 +3344,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1838,6 +3418,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1908,6 +3489,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1981,6 +3563,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2051,6 +3634,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2121,6 +3705,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2193,6 +3778,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2268,6 +3854,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2338,6 +3925,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2403,6 +3991,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2464,6 +4053,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2527,6 +4117,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2600,6 +4191,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2673,6 +4265,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2746,6 +4339,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2819,6 +4413,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2886,6 +4481,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2951,6 +4547,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3010,6 +4607,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3071,6 +4669,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3144,6 +4743,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3215,6 +4815,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3274,6 +4875,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3333,6 +4935,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3392,6 +4995,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tesla_fleet/snapshots/test_switch.ambr b/tests/components/tesla_fleet/snapshots/test_switch.ambr index 2d69a7d314a..2ea3bcc5ee5 100644 --- a/tests/components/tesla_fleet/snapshots/test_switch.ambr +++ b/tests/components/tesla_fleet/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -262,7 +268,7 @@ 'platform': 'tesla_fleet', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'charge_state_user_charge_enable_request', + 'translation_key': 'charge_state_charging_state', 'unique_id': 'LRWXF7EK4KC700000-charge_state_user_charge_enable_request', 'unit_of_measurement': None, }) @@ -278,7 +284,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_switch[switch.test_defrost-entry] @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -456,7 +464,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_switch_alt[switch.test_defrost-statealt] diff --git a/tests/components/tesla_fleet/test_binary_sensors.py b/tests/components/tesla_fleet/test_binary_sensor.py similarity index 100% rename from tests/components/tesla_fleet/test_binary_sensors.py rename to tests/components/tesla_fleet/test_binary_sensor.py diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index 7c17f986663..ff103ce03c2 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -21,6 +21,7 @@ from tesla_fleet_api.exceptions import ( from homeassistant.components.tesla_fleet.const import AUTHORIZE_URL from homeassistant.components.tesla_fleet.coordinator import ( + ENERGY_HISTORY_INTERVAL, ENERGY_INTERVAL, ENERGY_INTERVAL_SECONDS, VEHICLE_INTERVAL, @@ -30,6 +31,7 @@ from homeassistant.components.tesla_fleet.coordinator import ( from homeassistant.components.tesla_fleet.models import TeslaFleetData from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from . import setup_platform @@ -154,14 +156,15 @@ async def test_vehicle_refresh_offline( mock_vehicle_state.reset_mock() mock_vehicle_data.reset_mock() - # Then the vehicle goes offline + # Then the vehicle goes offline despite saying its online mock_vehicle_data.side_effect = VehicleOffline freezer.tick(VEHICLE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - mock_vehicle_state.assert_not_called() + mock_vehicle_state.assert_called_once() mock_vehicle_data.assert_called_once() + mock_vehicle_state.reset_mock() mock_vehicle_data.reset_mock() # And stays offline @@ -210,20 +213,15 @@ async def test_vehicle_refresh_ratelimited( assert (state := hass.states.get("sensor.test_battery_level")) assert state.state == "unknown" - assert mock_vehicle_data.call_count == 1 + + mock_vehicle_data.reset_mock() freezer.tick(VEHICLE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - # Should not call for another 10 seconds - assert mock_vehicle_data.call_count == 1 - - freezer.tick(VEHICLE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert mock_vehicle_data.call_count == 2 + assert (state := hass.states.get("sensor.test_battery_level")) + assert state.state == "unknown" async def test_vehicle_sleep( @@ -316,6 +314,21 @@ async def test_energy_site_refresh_error( assert normal_config_entry.state is state +# Test Energy History Coordinator +@pytest.mark.parametrize(("side_effect", "state"), ERRORS) +async def test_energy_history_refresh_error( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_energy_history: AsyncMock, + side_effect: TeslaFleetError, + state: ConfigEntryState, +) -> None: + """Test coordinator refresh with an error.""" + mock_energy_history.side_effect = side_effect + await setup_platform(hass, normal_config_entry) + assert normal_config_entry.state is state + + async def test_energy_live_refresh_ratelimited( hass: HomeAssistant, normal_config_entry: MockConfigEntry, @@ -378,6 +391,39 @@ async def test_energy_info_refresh_ratelimited( assert mock_site_info.call_count == 3 +async def test_energy_history_refresh_ratelimited( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_energy_history: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test coordinator refresh handles 429.""" + + await setup_platform(hass, normal_config_entry) + + mock_energy_history.side_effect = RateLimited( + {"after": int(ENERGY_HISTORY_INTERVAL.total_seconds() + 10)} + ) + freezer.tick(ENERGY_HISTORY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_energy_history.call_count == 2 + + freezer.tick(ENERGY_HISTORY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Should not call for another 10 seconds + assert mock_energy_history.call_count == 2 + + freezer.tick(ENERGY_HISTORY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_energy_history.call_count == 3 + + async def test_init_region_issue( hass: HomeAssistant, normal_config_entry: MockConfigEntry, @@ -424,3 +470,20 @@ async def test_signing( ) as mock_get_private_key: await setup_platform(hass, normal_config_entry) mock_get_private_key.assert_called_once() + + +async def test_bad_implementation( + hass: HomeAssistant, + bad_config_entry: MockConfigEntry, +) -> None: + """Test handling of a bad authentication implementation.""" + + await setup_platform(hass, bad_config_entry) + assert bad_config_entry.state is ConfigEntryState.SETUP_ERROR + + # Ensure reauth flow starts + assert any(bad_config_entry.async_get_active_flows(hass, {"reauth"})) + result = await bad_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert not result["errors"] diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index e10ae190a59..e4499d6e308 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -14,7 +14,7 @@ from homeassistant.components.tesla_wall_connector.const import ( ) from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -124,9 +124,9 @@ async def _test_sensors( for entity in entities_and_expected_values: state = hass.states.get(entity.entity_id) assert state, f"Unable to get state of {entity.entity_id}" - assert ( - state.state == entity.first_value - ), f"First update: {entity.entity_id} is expected to have state {entity.first_value} but has {state.state}" + assert state.state == entity.first_value, ( + f"First update: {entity.entity_id} is expected to have state {entity.first_value} but has {state.state}" + ) # Simulate second data update with ( @@ -147,6 +147,6 @@ async def _test_sensors( # Verify expected vs actual values of second update for entity in entities_and_expected_values: state = hass.states.get(entity.entity_id) - assert ( - state.state == entity.second_value - ), f"Second update: {entity.entity_id} is expected to have state {entity.second_value} but has {state.state}" + assert state.state == entity.second_value, ( + f"Second update: {entity.entity_id} is expected to have state {entity.second_value} but has {state.state}" + ) diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index a0c28262658..fc1f4199515 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -5,11 +5,11 @@ from unittest.mock import patch from tesla_wall_connector.exceptions import WallConnectorConnectionError from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.tesla_wall_connector.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -113,7 +113,7 @@ async def test_dhcp_can_finish( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="teslawallconnector_abc", ip="1.2.3.4", macaddress="aadc44271212", @@ -146,7 +146,7 @@ async def test_dhcp_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="teslawallconnector_aabbcc", ip="1.2.3.4", macaddress="aabbccddeeff", @@ -170,7 +170,7 @@ async def test_dhcp_error_from_wall_connector( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="teslawallconnector_aabbcc", ip="1.2.3.4", macaddress="aabbccddeeff", diff --git a/tests/components/teslemetry/__init__.py b/tests/components/teslemetry/__init__.py index b6b9df7eb4b..59727926f03 100644 --- a/tests/components/teslemetry/__init__.py +++ b/tests/components/teslemetry/__init__.py @@ -14,7 +14,9 @@ from .const import CONFIG from tests.common import MockConfigEntry -async def setup_platform(hass: HomeAssistant, platforms: list[Platform] | None = None): +async def setup_platform( + hass: HomeAssistant, platforms: list[Platform] | None = None +) -> MockConfigEntry: """Set up the Teslemetry platform.""" mock_entry = MockConfigEntry( @@ -32,6 +34,19 @@ async def setup_platform(hass: HomeAssistant, platforms: list[Platform] | None = return mock_entry +async def reload_platform( + hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform] | None = None +): + """Reload the Teslemetry platform.""" + + if platforms is None: + await hass.config_entries.async_reload(entry.entry_id) + else: + with patch("homeassistant.components.teslemetry.PLATFORMS", platforms): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + def assert_entities( hass: HomeAssistant, entry_id: str, diff --git a/tests/components/teslemetry/conftest.py b/tests/components/teslemetry/conftest.py index 256428aa703..e89bab9eff1 100644 --- a/tests/components/teslemetry/conftest.py +++ b/tests/components/teslemetry/conftest.py @@ -7,6 +7,7 @@ from copy import deepcopy from unittest.mock import AsyncMock, patch import pytest +from teslemetry_stream.stream import recursive_match from .const import ( COMMAND_OK, @@ -48,6 +49,15 @@ def mock_vehicle_data() -> Generator[AsyncMock]: yield mock_vehicle_data +@pytest.fixture +def mock_legacy(): + """Mock Tesla Fleet Api products method.""" + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.pre2021", return_value=True + ) as mock_pre2021: + yield mock_pre2021 + + @pytest.fixture(autouse=True) def mock_wake_up(): """Mock Tesla Fleet API Vehicle Specific wake_up method.""" @@ -109,9 +119,53 @@ def mock_energy_history(): @pytest.fixture(autouse=True) -def mock_listen(): +def mock_add_listener(): """Mock Teslemetry Stream listen method.""" with patch( - "homeassistant.components.teslemetry.TeslemetryStream.listen", - ) as mock_listen: - yield mock_listen + "homeassistant.components.teslemetry.TeslemetryStream.async_add_listener", + ) as mock_add_listener: + mock_add_listener.listeners = [] + + def unsubscribe() -> None: + return + + def side_effect(callback, filters): + mock_add_listener.listeners.append((callback, filters)) + return unsubscribe + + def send(event) -> None: + for listener, filters in mock_add_listener.listeners: + if recursive_match(filters, event): + listener(event) + + mock_add_listener.send = send + mock_add_listener.side_effect = side_effect + yield mock_add_listener + + +@pytest.fixture(autouse=True) +def mock_stream_get_config(): + """Mock Teslemetry Stream listen method.""" + with patch( + "teslemetry_stream.TeslemetryStreamVehicle.get_config", + ) as mock_stream_get_config: + yield mock_stream_get_config + + +@pytest.fixture(autouse=True) +def mock_stream_update_config(): + """Mock Teslemetry Stream listen method.""" + with patch( + "teslemetry_stream.TeslemetryStreamVehicle.update_config", + ) as mock_stream_update_config: + yield mock_stream_update_config + + +@pytest.fixture(autouse=True) +def mock_stream_connected(): + """Mock Teslemetry Stream listen method.""" + with patch( + "homeassistant.components.teslemetry.TeslemetryStream.connected", + return_value=True, + ) as mock_stream_connected: + yield mock_stream_connected diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py index bf483d576cd..40d55dab71f 100644 --- a/tests/components/teslemetry/const.py +++ b/tests/components/teslemetry/const.py @@ -18,6 +18,7 @@ VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN) LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN) ENERGY_HISTORY = load_json_object_fixture("energy_history.json", DOMAIN) +METADATA = load_json_object_fixture("metadata.json", DOMAIN) COMMAND_OK = {"response": {"result": True, "reason": ""}} COMMAND_REASON = {"response": {"result": False, "reason": "already closed"}} @@ -46,9 +47,25 @@ METADATA = { "energy_device_data", "energy_cmds", ], + "vehicles": { + "LRW3F7EK4NC700000": { + "proxy": False, + "access": True, + "polling": True, + "firmware": "2024.44.25", + } + }, } METADATA_NOSCOPE = { "uid": "abc-123", "region": "NA", "scopes": ["openid", "offline_access", "vehicle_device_data"], + "vehicles": { + "LRW3F7EK4NC700000": { + "proxy": False, + "access": True, + "polling": True, + "firmware": "2024.44.25", + } + }, } diff --git a/tests/components/teslemetry/fixtures/config.json b/tests/components/teslemetry/fixtures/config.json new file mode 100644 index 00000000000..0a6d2b11ab0 --- /dev/null +++ b/tests/components/teslemetry/fixtures/config.json @@ -0,0 +1,10 @@ +{ + "exp": 1749261108, + "hostname": "na.teslemetry.com", + "port": 4431, + "prefer_typed": true, + "pending": false, + "fields": { + "ChargeAmps": { "interval_seconds": 60 } + } +} diff --git a/tests/components/teslemetry/fixtures/metadata.json b/tests/components/teslemetry/fixtures/metadata.json new file mode 100644 index 00000000000..60282afc934 --- /dev/null +++ b/tests/components/teslemetry/fixtures/metadata.json @@ -0,0 +1,22 @@ +{ + "uid": "abc-123", + "region": "NA", + "scopes": [ + "openid", + "offline_access", + "user_data", + "vehicle_device_data", + "vehicle_cmds", + "vehicle_charging_cmds", + "energy_device_data", + "energy_cmds" + ], + "vehicles": { + "LRW3F7EK4NC700000": { + "access": true, + "polling": true, + "proxy": true, + "firmware": "2024.44.25" + } + } +} diff --git a/tests/components/teslemetry/fixtures/vehicle_data.json b/tests/components/teslemetry/fixtures/vehicle_data.json index fcfa0707b2c..0cd238c4e52 100644 --- a/tests/components/teslemetry/fixtures/vehicle_data.json +++ b/tests/components/teslemetry/fixtures/vehicle_data.json @@ -192,7 +192,7 @@ "api_version": 71, "autopark_state_v2": "unavailable", "calendar_supported": true, - "car_version": "2023.44.30.8 06f534d46010", + "car_version": "2024.44.25 06f534d46010", "center_display_state": 0, "dashcam_clip_save_available": true, "dashcam_state": "Recording", diff --git a/tests/components/teslemetry/fixtures/vehicle_data_alt.json b/tests/components/teslemetry/fixtures/vehicle_data_alt.json index 5ef5ea92a74..ec524614d49 100644 --- a/tests/components/teslemetry/fixtures/vehicle_data_alt.json +++ b/tests/components/teslemetry/fixtures/vehicle_data_alt.json @@ -35,7 +35,7 @@ "charge_port_cold_weather_mode": false, "charge_port_color": "", "charge_port_door_open": true, - "charge_port_latch": "Engaged", + "charge_port_latch": null, "charge_rate": 0, "charger_actual_current": 0, "charger_phases": null, @@ -190,7 +190,7 @@ "api_version": 71, "autopark_state_v2": "unavailable", "calendar_supported": true, - "car_version": "2023.44.30.8 06f534d46010", + "car_version": "2024.44.25 06f534d46010", "center_display_state": 0, "dashcam_clip_save_available": true, "dashcam_state": "Recording", diff --git a/tests/components/teslemetry/snapshots/test_binary_sensors.ambr b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr similarity index 52% rename from tests/components/teslemetry/snapshots/test_binary_sensors.ambr rename to tests/components/teslemetry/snapshots/test_binary_sensor.ambr index 95330840109..6a6e9826dc2 100644 --- a/tests/components/teslemetry/snapshots/test_binary_sensors.ambr +++ b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -183,6 +187,100 @@ 'state': 'off', }) # --- +# name: test_binary_sensor[binary_sensor.test_automatic_blind_spot_camera-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_automatic_blind_spot_camera', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic blind spot camera', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'automatic_blind_spot_camera', + 'unique_id': 'LRW3F7EK4NC700000-automatic_blind_spot_camera', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_automatic_blind_spot_camera-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Automatic blind spot camera', + }), + 'context': , + 'entity_id': 'binary_sensor.test_automatic_blind_spot_camera', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_automatic_emergency_braking_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_automatic_emergency_braking_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic emergency braking off', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'automatic_emergency_braking_off', + 'unique_id': 'LRW3F7EK4NC700000-automatic_emergency_braking_off', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_automatic_emergency_braking_off-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Automatic emergency braking off', + }), + 'context': , + 'entity_id': 'binary_sensor.test_automatic_emergency_braking_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor[binary_sensor.test_battery_heater-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -190,6 +288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -227,22 +326,164 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- -# name: test_binary_sensor[binary_sensor.test_cabin_overheat_protection_actively_cooling-entry] +# name: test_binary_sensor[binary_sensor.test_blind_spot_collision_warning_chime-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_blind_spot_collision_warning_chime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Blind spot collision warning chime', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'blind_spot_collision_warning_chime', + 'unique_id': 'LRW3F7EK4NC700000-blind_spot_collision_warning_chime', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_blind_spot_collision_warning_chime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blind spot collision warning chime', + }), + 'context': , + 'entity_id': 'binary_sensor.test_blind_spot_collision_warning_chime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_bms_full_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_bms_full_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'BMS full charge', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bms_full_charge_complete', + 'unique_id': 'LRW3F7EK4NC700000-bms_full_charge_complete', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_bms_full_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test BMS full charge', + }), + 'context': , + 'entity_id': 'binary_sensor.test_bms_full_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_brake_pedal-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_brake_pedal', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Brake pedal', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'brake_pedal', + 'unique_id': 'LRW3F7EK4NC700000-brake_pedal', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_brake_pedal-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Brake pedal', + }), + 'context': , + 'entity_id': 'binary_sensor.test_brake_pedal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_cabin_overheat_protection_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', + 'entity_id': 'binary_sensor.test_cabin_overheat_protection_active', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -254,7 +495,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cabin overheat protection actively cooling', + 'original_name': 'Cabin overheat protection active', 'platform': 'teslemetry', 'previous_unique_id': None, 'supported_features': 0, @@ -263,14 +504,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[binary_sensor.test_cabin_overheat_protection_actively_cooling-state] +# name: test_binary_sensor[binary_sensor.test_cabin_overheat_protection_active-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'heat', - 'friendly_name': 'Test Cabin overheat protection actively cooling', + 'friendly_name': 'Test Cabin overheat protection active', }), 'context': , - 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', + 'entity_id': 'binary_sensor.test_cabin_overheat_protection_active', 'last_changed': , 'last_reported': , 'last_updated': , @@ -284,6 +525,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -324,6 +566,53 @@ 'state': 'on', }) # --- +# name: test_binary_sensor[binary_sensor.test_charge_port_cold_weather_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_charge_port_cold_weather_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge port cold weather mode', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_port_cold_weather_mode', + 'unique_id': 'LRW3F7EK4NC700000-charge_port_cold_weather_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_charge_port_cold_weather_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge port cold weather mode', + }), + 'context': , + 'entity_id': 'binary_sensor.test_charge_port_cold_weather_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor[binary_sensor.test_charger_has_multiple_phases-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -331,6 +620,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -367,7 +657,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_dashcam-entry] @@ -377,6 +667,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -417,6 +708,335 @@ 'state': 'on', }) # --- +# name: test_binary_sensor[binary_sensor.test_dc_to_dc_converter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_dc_to_dc_converter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'DC to DC converter', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dc_dc_enable', + 'unique_id': 'LRW3F7EK4NC700000-dc_dc_enable', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_dc_to_dc_converter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test DC to DC converter', + }), + 'context': , + 'entity_id': 'binary_sensor.test_dc_to_dc_converter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_drive_rail-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_drive_rail', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Drive rail', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_rail', + 'unique_id': 'LRW3F7EK4NC700000-drive_rail', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_drive_rail-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Drive rail', + }), + 'context': , + 'entity_id': 'binary_sensor.test_drive_rail', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_driver_seat_belt-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_driver_seat_belt', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Driver seat belt', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'driver_seat_belt', + 'unique_id': 'LRW3F7EK4NC700000-driver_seat_belt', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_driver_seat_belt-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Driver seat belt', + }), + 'context': , + 'entity_id': 'binary_sensor.test_driver_seat_belt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_driver_seat_occupied-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_driver_seat_occupied', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Driver seat occupied', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'driver_seat_occupied', + 'unique_id': 'LRW3F7EK4NC700000-driver_seat_occupied', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_driver_seat_occupied-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Driver seat occupied', + }), + 'context': , + 'entity_id': 'binary_sensor.test_driver_seat_occupied', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_emergency_lane_departure_avoidance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_emergency_lane_departure_avoidance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Emergency lane departure avoidance', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'emergency_lane_departure_avoidance', + 'unique_id': 'LRW3F7EK4NC700000-emergency_lane_departure_avoidance', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_emergency_lane_departure_avoidance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Emergency lane departure avoidance', + }), + 'context': , + 'entity_id': 'binary_sensor.test_emergency_lane_departure_avoidance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_european_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_european_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'European vehicle', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'europe_vehicle', + 'unique_id': 'LRW3F7EK4NC700000-europe_vehicle', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_european_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test European vehicle', + }), + 'context': , + 'entity_id': 'binary_sensor.test_european_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_fast_charger_present-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_fast_charger_present', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fast charger present', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fast_charger_present', + 'unique_id': 'LRW3F7EK4NC700000-fast_charger_present', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_fast_charger_present-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fast charger present', + }), + 'context': , + 'entity_id': 'binary_sensor.test_fast_charger_present', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor[binary_sensor.test_front_driver_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -424,6 +1044,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -461,7 +1082,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_front_driver_window-entry] @@ -471,6 +1092,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -508,7 +1130,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_front_passenger_door-entry] @@ -518,6 +1140,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -555,7 +1178,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_front_passenger_window-entry] @@ -565,6 +1188,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -602,7 +1226,290 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_gps_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_gps_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'GPS state', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gps_state', + 'unique_id': 'LRW3F7EK4NC700000-gps_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_gps_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Test GPS state', + }), + 'context': , + 'entity_id': 'binary_sensor.test_gps_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_guest_mode_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_guest_mode_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Guest mode enabled', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'guest_mode_enabled', + 'unique_id': 'LRW3F7EK4NC700000-guest_mode_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_guest_mode_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Guest mode enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.test_guest_mode_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_homelink_nearby-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_homelink_nearby', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Homelink nearby', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'homelink_nearby', + 'unique_id': 'LRW3F7EK4NC700000-homelink_nearby', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_homelink_nearby-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Homelink nearby', + }), + 'context': , + 'entity_id': 'binary_sensor.test_homelink_nearby', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_offroad_lightbar-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_offroad_lightbar', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Offroad lightbar', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'offroad_lightbar_present', + 'unique_id': 'LRW3F7EK4NC700000-offroad_lightbar_present', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_offroad_lightbar-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Offroad lightbar', + }), + 'context': , + 'entity_id': 'binary_sensor.test_offroad_lightbar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_passenger_seat_belt-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_passenger_seat_belt', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Passenger seat belt', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'passenger_seat_belt', + 'unique_id': 'LRW3F7EK4NC700000-passenger_seat_belt', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_passenger_seat_belt-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Passenger seat belt', + }), + 'context': , + 'entity_id': 'binary_sensor.test_passenger_seat_belt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_pin_to_drive_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_pin_to_drive_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pin to drive enabled', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pin_to_drive_enabled', + 'unique_id': 'LRW3F7EK4NC700000-pin_to_drive_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_pin_to_drive_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pin to drive enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.test_pin_to_drive_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_preconditioning-entry] @@ -612,6 +1519,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -658,6 +1566,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -694,7 +1603,54 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_display_hvac-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_rear_display_hvac', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Rear display HVAC', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'rear_display_hvac_enabled', + 'unique_id': 'LRW3F7EK4NC700000-rear_display_hvac_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_rear_display_hvac-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Rear display HVAC', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_display_hvac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_rear_driver_door-entry] @@ -704,6 +1660,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -741,7 +1698,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_rear_driver_window-entry] @@ -751,6 +1708,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -788,7 +1746,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_rear_passenger_door-entry] @@ -798,6 +1756,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -835,7 +1794,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_rear_passenger_window-entry] @@ -845,6 +1804,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -882,7 +1842,54 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_right_hand_drive-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_right_hand_drive', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Right hand drive', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'right_hand_drive', + 'unique_id': 'LRW3F7EK4NC700000-right_hand_drive', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_right_hand_drive-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Right hand drive', + }), + 'context': , + 'entity_id': 'binary_sensor.test_right_hand_drive', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_scheduled_charging_pending-entry] @@ -892,6 +1899,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -928,7 +1936,54 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_service_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_service_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Service mode', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'service_mode', + 'unique_id': 'LRW3F7EK4NC700000-service_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_service_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Service mode', + }), + 'context': , + 'entity_id': 'binary_sensor.test_service_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_status-entry] @@ -938,6 +1993,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -978,6 +2034,53 @@ 'state': 'on', }) # --- +# name: test_binary_sensor[binary_sensor.test_supercharger_session_trip_planner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_supercharger_session_trip_planner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Supercharger session trip planner', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'supercharger_session_trip_planner', + 'unique_id': 'LRW3F7EK4NC700000-supercharger_session_trip_planner', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_supercharger_session_trip_planner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Supercharger session trip planner', + }), + 'context': , + 'entity_id': 'binary_sensor.test_supercharger_session_trip_planner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor[binary_sensor.test_tire_pressure_warning_front_left-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -985,6 +2088,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1032,6 +2136,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1079,6 +2184,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1126,6 +2232,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1173,6 +2280,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1219,6 +2327,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1259,6 +2368,53 @@ 'state': 'off', }) # --- +# name: test_binary_sensor[binary_sensor.test_wiper_heat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_wiper_heat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wiper heat', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wiper_heat_enabled', + 'unique_id': 'LRW3F7EK4NC700000-wiper_heat_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_wiper_heat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Wiper heat', + }), + 'context': , + 'entity_id': 'binary_sensor.test_wiper_heat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor_refresh[binary_sensor.energy_site_backup_capable-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -1311,6 +2467,32 @@ 'state': 'off', }) # --- +# name: test_binary_sensor_refresh[binary_sensor.test_automatic_blind_spot_camera-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Automatic blind spot camera', + }), + 'context': , + 'entity_id': 'binary_sensor.test_automatic_blind_spot_camera', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_automatic_emergency_braking_off-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Automatic emergency braking off', + }), + 'context': , + 'entity_id': 'binary_sensor.test_automatic_emergency_braking_off', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor_refresh[binary_sensor.test_battery_heater-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -1322,17 +2504,56 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- -# name: test_binary_sensor_refresh[binary_sensor.test_cabin_overheat_protection_actively_cooling-statealt] +# name: test_binary_sensor_refresh[binary_sensor.test_blind_spot_collision_warning_chime-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Blind spot collision warning chime', + }), + 'context': , + 'entity_id': 'binary_sensor.test_blind_spot_collision_warning_chime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_bms_full_charge-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test BMS full charge', + }), + 'context': , + 'entity_id': 'binary_sensor.test_bms_full_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_brake_pedal-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Brake pedal', + }), + 'context': , + 'entity_id': 'binary_sensor.test_brake_pedal', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_cabin_overheat_protection_active-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'heat', - 'friendly_name': 'Test Cabin overheat protection actively cooling', + 'friendly_name': 'Test Cabin overheat protection active', }), 'context': , - 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', + 'entity_id': 'binary_sensor.test_cabin_overheat_protection_active', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1353,6 +2574,19 @@ 'state': 'on', }) # --- +# name: test_binary_sensor_refresh[binary_sensor.test_charge_port_cold_weather_mode-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge port cold weather mode', + }), + 'context': , + 'entity_id': 'binary_sensor.test_charge_port_cold_weather_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor_refresh[binary_sensor.test_charger_has_multiple_phases-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -1363,7 +2597,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_dashcam-statealt] @@ -1380,6 +2614,97 @@ 'state': 'on', }) # --- +# name: test_binary_sensor_refresh[binary_sensor.test_dc_to_dc_converter-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test DC to DC converter', + }), + 'context': , + 'entity_id': 'binary_sensor.test_dc_to_dc_converter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_drive_rail-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Drive rail', + }), + 'context': , + 'entity_id': 'binary_sensor.test_drive_rail', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_driver_seat_belt-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Driver seat belt', + }), + 'context': , + 'entity_id': 'binary_sensor.test_driver_seat_belt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_driver_seat_occupied-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Driver seat occupied', + }), + 'context': , + 'entity_id': 'binary_sensor.test_driver_seat_occupied', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_emergency_lane_departure_avoidance-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Emergency lane departure avoidance', + }), + 'context': , + 'entity_id': 'binary_sensor.test_emergency_lane_departure_avoidance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_european_vehicle-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test European vehicle', + }), + 'context': , + 'entity_id': 'binary_sensor.test_european_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_fast_charger_present-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fast charger present', + }), + 'context': , + 'entity_id': 'binary_sensor.test_fast_charger_present', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor_refresh[binary_sensor.test_front_driver_door-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -1391,7 +2716,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_front_driver_window-statealt] @@ -1405,7 +2730,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_front_passenger_door-statealt] @@ -1419,7 +2744,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_front_passenger_window-statealt] @@ -1433,7 +2758,86 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_gps_state-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Test GPS state', + }), + 'context': , + 'entity_id': 'binary_sensor.test_gps_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_guest_mode_enabled-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Guest mode enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.test_guest_mode_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_homelink_nearby-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Homelink nearby', + }), + 'context': , + 'entity_id': 'binary_sensor.test_homelink_nearby', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_offroad_lightbar-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Offroad lightbar', + }), + 'context': , + 'entity_id': 'binary_sensor.test_offroad_lightbar', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_passenger_seat_belt-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Passenger seat belt', + }), + 'context': , + 'entity_id': 'binary_sensor.test_passenger_seat_belt', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_pin_to_drive_enabled-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pin to drive enabled', + }), + 'context': , + 'entity_id': 'binary_sensor.test_pin_to_drive_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_preconditioning-statealt] @@ -1459,7 +2863,20 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_rear_display_hvac-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Rear display HVAC', + }), + 'context': , + 'entity_id': 'binary_sensor.test_rear_display_hvac', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_rear_driver_door-statealt] @@ -1473,7 +2890,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_rear_driver_window-statealt] @@ -1487,7 +2904,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_rear_passenger_door-statealt] @@ -1501,7 +2918,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_rear_passenger_window-statealt] @@ -1515,7 +2932,20 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_right_hand_drive-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Right hand drive', + }), + 'context': , + 'entity_id': 'binary_sensor.test_right_hand_drive', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_scheduled_charging_pending-statealt] @@ -1528,7 +2958,20 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_refresh[binary_sensor.test_service_mode-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Service mode', + }), + 'context': , + 'entity_id': 'binary_sensor.test_service_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_status-statealt] @@ -1545,6 +2988,19 @@ 'state': 'on', }) # --- +# name: test_binary_sensor_refresh[binary_sensor.test_supercharger_session_trip_planner-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Supercharger session trip planner', + }), + 'context': , + 'entity_id': 'binary_sensor.test_supercharger_session_trip_planner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor_refresh[binary_sensor.test_tire_pressure_warning_front_left-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -1628,3 +3084,31 @@ 'state': 'on', }) # --- +# name: test_binary_sensor_refresh[binary_sensor.test_wiper_heat-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Wiper heat', + }), + 'context': , + 'entity_id': 'binary_sensor.test_wiper_heat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensors_streaming[binary_sensor.test_driver_seat_belt-state] + 'off' +# --- +# name: test_binary_sensors_streaming[binary_sensor.test_front_driver_door-state] + 'off' +# --- +# name: test_binary_sensors_streaming[binary_sensor.test_front_driver_window-state] + 'on' +# --- +# name: test_binary_sensors_streaming[binary_sensor.test_front_passenger_door-state] + 'off' +# --- +# name: test_binary_sensors_streaming[binary_sensor.test_front_passenger_window-state] + 'on' +# --- diff --git a/tests/components/teslemetry/snapshots/test_button.ambr b/tests/components/teslemetry/snapshots/test_button.ambr index 6d3016186ae..e4e20215020 100644 --- a/tests/components/teslemetry/snapshots/test_button.ambr +++ b/tests/components/teslemetry/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/teslemetry/snapshots/test_climate.ambr b/tests/components/teslemetry/snapshots/test_climate.ambr index 7064309e98b..4c265c00cb8 100644 --- a/tests/components/teslemetry/snapshots/test_climate.ambr +++ b/tests/components/teslemetry/snapshots/test_climate.ambr @@ -21,6 +21,7 @@ 'target_temp_step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -91,6 +92,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -162,6 +164,7 @@ 'target_temp_step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -231,6 +234,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -300,6 +304,7 @@ 'target_temp_step': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -339,6 +344,7 @@ 'min_temp': 15.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/teslemetry/snapshots/test_cover.ambr b/tests/components/teslemetry/snapshots/test_cover.ambr index 24e1b02a5f8..9548a911cf9 100644 --- a/tests/components/teslemetry/snapshots/test_cover.ambr +++ b/tests/components/teslemetry/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -246,6 +251,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +300,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,54 +342,6 @@ 'state': 'open', }) # --- -# name: test_cover_alt[cover.test_sunroof-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'cover', - 'entity_category': None, - 'entity_id': 'cover.test_sunroof', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Sunroof', - 'platform': 'teslemetry', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': 'vehicle_state_sun_roof_state', - 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_sun_roof_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_cover_alt[cover.test_sunroof-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'window', - 'friendly_name': 'Test Sunroof', - 'supported_features': , - }), - 'context': , - 'entity_id': 'cover.test_sunroof', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_cover_alt[cover.test_trunk-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -390,6 +349,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -438,6 +398,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -486,6 +447,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -534,6 +496,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -582,6 +545,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -630,6 +594,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -678,6 +643,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -719,3 +685,39 @@ 'state': 'closed', }) # --- +# name: test_cover_streaming[cover.test_charge_port_door-closed] + 'closed' +# --- +# name: test_cover_streaming[cover.test_charge_port_door-open] + 'closed' +# --- +# name: test_cover_streaming[cover.test_charge_port_door-unknown] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_frunk-closed] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_frunk-open] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_frunk-unknown] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_trunk-closed] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_trunk-open] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_trunk-unknown] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_windows-closed] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_windows-open] + 'unknown' +# --- +# name: test_cover_streaming[cover.test_windows-unknown] + 'unknown' +# --- diff --git a/tests/components/teslemetry/snapshots/test_device_tracker.ambr b/tests/components/teslemetry/snapshots/test_device_tracker.ambr index ac4c388873f..b9e381ee42d 100644 --- a/tests/components/teslemetry/snapshots/test_device_tracker.ambr +++ b/tests/components/teslemetry/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -133,3 +135,21 @@ 'state': 'not_home', }) # --- +# name: test_device_tracker_streaming[device_tracker.test_location-restore] + 'not_home' +# --- +# name: test_device_tracker_streaming[device_tracker.test_location-state] + 'not_home' +# --- +# name: test_device_tracker_streaming[device_tracker.test_origin-restore] + 'unknown' +# --- +# name: test_device_tracker_streaming[device_tracker.test_origin-state] + 'unknown' +# --- +# name: test_device_tracker_streaming[device_tracker.test_route-restore] + 'not_home' +# --- +# name: test_device_tracker_streaming[device_tracker.test_route-state] + 'home' +# --- diff --git a/tests/components/teslemetry/snapshots/test_diagnostics.ambr b/tests/components/teslemetry/snapshots/test_diagnostics.ambr index 3b96d6f70c0..16cabfddd09 100644 --- a/tests/components/teslemetry/snapshots/test_diagnostics.ambr +++ b/tests/components/teslemetry/snapshots/test_diagnostics.ambr @@ -352,7 +352,7 @@ 'vehicle_state_api_version': 71, 'vehicle_state_autopark_state_v2': 'unavailable', 'vehicle_state_calendar_supported': True, - 'vehicle_state_car_version': '2023.44.30.8 06f534d46010', + 'vehicle_state_car_version': '2024.44.25 06f534d46010', 'vehicle_state_center_display_state': 0, 'vehicle_state_dashcam_clip_save_available': True, 'vehicle_state_dashcam_state': 'Recording', diff --git a/tests/components/teslemetry/snapshots/test_init.ambr b/tests/components/teslemetry/snapshots/test_init.ambr index 7d60ed82859..f1011034d63 100644 --- a/tests/components/teslemetry/snapshots/test_init.ambr +++ b/tests/components/teslemetry/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://teslemetry.com/console', 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://teslemetry.com/console', 'connections': set({ }), @@ -67,6 +69,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://teslemetry.com/console', 'connections': set({ }), @@ -99,6 +102,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://teslemetry.com/console', 'connections': set({ }), diff --git a/tests/components/teslemetry/snapshots/test_lock.ambr b/tests/components/teslemetry/snapshots/test_lock.ambr index 2130c4d9574..d6b29f0d7d4 100644 --- a/tests/components/teslemetry/snapshots/test_lock.ambr +++ b/tests/components/teslemetry/snapshots/test_lock.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -93,3 +95,111 @@ 'state': 'unlocked', }) # --- +# name: test_lock_alt[lock.test_charge_cable_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.test_charge_cable_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge cable lock', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charge_port_latch', + 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_port_latch', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock_alt[lock.test_charge_cable_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge cable lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.test_charge_cable_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unlocked', + }) +# --- +# name: test_lock_alt[lock.test_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.test_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_locked', + 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_locked', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock_alt[lock.test_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.test_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unlocked', + }) +# --- +# name: test_lock_streaming[lock.test_charge_cable_lock-locked] + 'locked' +# --- +# name: test_lock_streaming[lock.test_charge_cable_lock-unlocked] + 'unlocked' +# --- +# name: test_lock_streaming[lock.test_lock-locked] + 'locked' +# --- +# name: test_lock_streaming[lock.test_lock-unlocked] + 'unlocked' +# --- diff --git a/tests/components/teslemetry/snapshots/test_media_player.ambr b/tests/components/teslemetry/snapshots/test_media_player.ambr index a9d2569c637..663e91a502c 100644 --- a/tests/components/teslemetry/snapshots/test_media_player.ambr +++ b/tests/components/teslemetry/snapshots/test_media_player.ambr @@ -7,6 +7,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -84,6 +85,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/teslemetry/snapshots/test_number.ambr b/tests/components/teslemetry/snapshots/test_number.ambr index 0f30daf635e..5ca9feb22f2 100644 --- a/tests/components/teslemetry/snapshots/test_number.ambr +++ b/tests/components/teslemetry/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -127,6 +129,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -184,6 +187,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -229,3 +233,9 @@ 'state': '80', }) # --- +# name: test_number_streaming[number.test_charge_current-state] + '24' +# --- +# name: test_number_streaming[number.test_charge_limit-state] + '99' +# --- diff --git a/tests/components/teslemetry/snapshots/test_select.ambr b/tests/components/teslemetry/snapshots/test_select.ambr index 0c2547f309d..755a1a82c41 100644 --- a/tests/components/teslemetry/snapshots/test_select.ambr +++ b/tests/components/teslemetry/snapshots/test_select.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -127,6 +129,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -186,6 +189,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -245,6 +249,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -304,6 +309,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -363,6 +369,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -408,3 +415,79 @@ 'state': 'off', }) # --- +# name: test_select[select.test_steering_wheel_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'low', + 'high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.test_steering_wheel_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Steering wheel heater', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_steering_wheel_heat_level', + 'unique_id': 'LRW3F7EK4NC700000-climate_state_steering_wheel_heat_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[select.test_steering_wheel_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Steering wheel heater', + 'options': list([ + 'off', + 'low', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.test_steering_wheel_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_select_streaming[select.test_seat_heater_front_left] + 'off' +# --- +# name: test_select_streaming[select.test_seat_heater_front_right] + 'low' +# --- +# name: test_select_streaming[select.test_seat_heater_rear_center] + 'unknown' +# --- +# name: test_select_streaming[select.test_seat_heater_rear_left] + 'medium' +# --- +# name: test_select_streaming[select.test_seat_heater_rear_right] + 'high' +# --- +# name: test_select_streaming[select.test_steering_wheel_heater] + 'off' +# --- diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index acff157bfea..c5d98abc95c 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -81,6 +82,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -154,6 +156,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -227,6 +230,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -300,6 +304,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -373,6 +378,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -446,6 +452,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -519,6 +526,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -592,6 +600,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -665,6 +674,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -738,6 +748,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -811,6 +822,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -884,6 +896,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -957,6 +970,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1030,6 +1044,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1103,6 +1118,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1176,6 +1192,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1249,6 +1266,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1322,6 +1340,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1395,6 +1414,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1468,6 +1488,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1541,6 +1562,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1614,6 +1636,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1687,6 +1710,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1766,6 +1790,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1843,6 +1868,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1916,6 +1942,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1986,6 +2013,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2059,6 +2087,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2132,6 +2161,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2205,6 +2235,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2276,6 +2307,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2335,6 +2367,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2400,6 +2433,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2414,6 +2448,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2467,6 +2504,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2538,6 +2576,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2599,6 +2638,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2669,6 +2709,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2739,6 +2780,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2806,6 +2848,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2873,6 +2916,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2947,6 +2991,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3026,6 +3071,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3096,6 +3142,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3166,6 +3213,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3237,6 +3285,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3298,6 +3347,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3371,6 +3421,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3441,6 +3492,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3514,6 +3566,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3584,6 +3637,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3654,6 +3708,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3726,6 +3781,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3801,6 +3857,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3843,7 +3900,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unavailable', }) # --- # name: test_sensors[sensor.test_speed-statealt] @@ -3859,7 +3916,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unavailable', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-entry] @@ -3871,6 +3928,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3910,7 +3968,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-statealt] @@ -3926,7 +3984,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- # name: test_sensors[sensor.test_time_to_arrival-entry] @@ -3936,6 +3994,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3997,6 +4056,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4060,6 +4120,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4133,6 +4194,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4206,6 +4268,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4279,6 +4342,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4352,6 +4416,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4419,6 +4484,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4484,6 +4550,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4543,6 +4610,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4604,6 +4672,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4677,6 +4746,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4748,6 +4818,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4807,6 +4878,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4866,6 +4938,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4925,6 +4998,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -4977,3 +5051,24 @@ 'state': 'unknown', }) # --- +# name: test_sensors_streaming[sensor.test_battery_level-state] + '90' +# --- +# name: test_sensors_streaming[sensor.test_charge_cable-state] + 'unknown' +# --- +# name: test_sensors_streaming[sensor.test_charge_energy_added-state] + '10' +# --- +# name: test_sensors_streaming[sensor.test_charger_power-state] + '2' +# --- +# name: test_sensors_streaming[sensor.test_charging-state] + 'charging' +# --- +# name: test_sensors_streaming[sensor.test_time_to_arrival-state] + 'unknown' +# --- +# name: test_sensors_streaming[sensor.test_time_to_full_charge-state] + 'unknown' +# --- diff --git a/tests/components/teslemetry/snapshots/test_switch.ambr b/tests/components/teslemetry/snapshots/test_switch.ambr index 5693d4bdd5e..f9997133044 100644 --- a/tests/components/teslemetry/snapshots/test_switch.ambr +++ b/tests/components/teslemetry/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -262,7 +268,7 @@ 'platform': 'teslemetry', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'charge_state_user_charge_enable_request', + 'translation_key': 'charge_state_charging_state', 'unique_id': 'LRW3F7EK4NC700000-charge_state_user_charge_enable_request', 'unit_of_measurement': None, }) @@ -278,7 +284,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_switch[switch.test_defrost-entry] @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -456,7 +464,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_switch_alt[switch.test_defrost-statealt] diff --git a/tests/components/teslemetry/snapshots/test_update.ambr b/tests/components/teslemetry/snapshots/test_update.ambr index 0777f4ccdb9..1c7d525af86 100644 --- a/tests/components/teslemetry/snapshots/test_update.ambr +++ b/tests/components/teslemetry/snapshots/test_update.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -40,7 +41,7 @@ 'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png', 'friendly_name': 'Test Update', 'in_progress': False, - 'installed_version': '2023.44.30.8', + 'installed_version': '2024.44.25', 'latest_version': '2024.12.0.0', 'release_summary': None, 'release_url': None, @@ -54,7 +55,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- # name: test_update_alt[update.test_update-entry] @@ -64,6 +65,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,8 +100,8 @@ 'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png', 'friendly_name': 'Test Update', 'in_progress': False, - 'installed_version': '2023.44.30.8', - 'latest_version': '2023.44.30.8', + 'installed_version': '2024.44.25', + 'latest_version': '2024.44.25', 'release_summary': None, 'release_url': None, 'skipped_version': None, diff --git a/tests/components/teslemetry/test_binary_sensor.py b/tests/components/teslemetry/test_binary_sensor.py new file mode 100644 index 00000000000..5a7126afe1b --- /dev/null +++ b/tests/components/teslemetry/test_binary_sensor.py @@ -0,0 +1,106 @@ +"""Test the Teslemetry binary sensor platform.""" + +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion +from teslemetry_stream import Signal + +from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import assert_entities, assert_entities_alt, setup_platform +from .const import VEHICLE_DATA_ALT + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Tests that the binary sensor entities are correct.""" + + entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) + assert_entities(hass, entry.entry_id, entity_registry, snapshot) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor_refresh( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_vehicle_data: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Tests that the binary sensor entities are correct.""" + + entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) + + # Refresh + mock_vehicle_data.return_value = VEHICLE_DATA_ALT + freezer.tick(VEHICLE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensors_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the binary sensor entities with streaming are correct.""" + + freezer.move_to("2024-01-01 00:00:00+00:00") + + entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.FD_WINDOW: "WindowStateOpened", + Signal.FP_WINDOW: "INVALID_VALUE", + Signal.DOOR_STATE: { + "DoorState": { + "DriverFront": True, + "DriverRear": False, + "PassengerFront": False, + "PassengerRear": False, + "TrunkFront": False, + "TrunkRear": False, + } + }, + Signal.DRIVER_SEAT_BELT: None, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + # Reload the entry + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + # Assert the entities restored their values + for entity_id in ( + "binary_sensor.test_front_driver_window", + "binary_sensor.test_front_passenger_window", + "binary_sensor.test_front_driver_door", + "binary_sensor.test_front_passenger_door", + "binary_sensor.test_driver_seat_belt", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-state") diff --git a/tests/components/teslemetry/test_binary_sensors.py b/tests/components/teslemetry/test_binary_sensors.py deleted file mode 100644 index 0a47dce9537..00000000000 --- a/tests/components/teslemetry/test_binary_sensors.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Test the Teslemetry binary sensor platform.""" - -from unittest.mock import AsyncMock - -from freezegun.api import FrozenDateTimeFactory -import pytest -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import assert_entities, assert_entities_alt, setup_platform -from .const import VEHICLE_DATA_ALT - -from tests.common import async_fire_time_changed - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_binary_sensor( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - entity_registry: er.EntityRegistry, -) -> None: - """Tests that the binary sensor entities are correct.""" - - entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) - assert_entities(hass, entry.entry_id, entity_registry, snapshot) - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_binary_sensor_refresh( - hass: HomeAssistant, - snapshot: SnapshotAssertion, - entity_registry: er.EntityRegistry, - mock_vehicle_data: AsyncMock, - freezer: FrozenDateTimeFactory, -) -> None: - """Tests that the binary sensor entities are correct.""" - - entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) - - # Refresh - mock_vehicle_data.return_value = VEHICLE_DATA_ALT - freezer.tick(VEHICLE_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot) diff --git a/tests/components/teslemetry/test_button.py b/tests/components/teslemetry/test_button.py index 04edf668765..75f94342f1e 100644 --- a/tests/components/teslemetry/test_button.py +++ b/tests/components/teslemetry/test_button.py @@ -29,6 +29,7 @@ async def test_button( @pytest.mark.parametrize( ("name", "func"), [ + ("wake", "wake_up"), ("flash_lights", "flash_lights"), ("honk_horn", "honk_horn"), ("keyless_driving", "remote_start_drive"), diff --git a/tests/components/teslemetry/test_cover.py b/tests/components/teslemetry/test_cover.py index 7dbdcfa5747..14af1e732fe 100644 --- a/tests/components/teslemetry/test_cover.py +++ b/tests/components/teslemetry/test_cover.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest from syrupy.assertion import SnapshotAssertion +from teslemetry_stream import Signal from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, @@ -25,6 +26,7 @@ async def test_cover( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_legacy: AsyncMock, ) -> None: """Tests that the cover entities are correct.""" @@ -38,6 +40,7 @@ async def test_cover_alt( snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, mock_vehicle_data: AsyncMock, + mock_legacy: AsyncMock, ) -> None: """Tests that the cover entities are correct with alternate values.""" @@ -52,6 +55,7 @@ async def test_cover_noscope( snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, mock_metadata: AsyncMock, + mock_legacy: AsyncMock, ) -> None: """Tests that the cover entities are correct without scopes.""" @@ -215,3 +219,127 @@ async def test_cover_services( state = hass.states.get(entity_id) assert state assert state.state == CoverState.CLOSED + + +async def test_cover_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the binary sensor entities with streaming are correct.""" + + entry = await setup_platform(hass, [Platform.COVER]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.FD_WINDOW: "WindowStateClosed", + Signal.FP_WINDOW: "WindowStateClosed", + Signal.RD_WINDOW: "WindowStateClosed", + Signal.RP_WINDOW: "WindowStateClosed", + Signal.CHARGE_PORT_DOOR_OPEN: False, + Signal.DOOR_STATE: { + "DoorState": { + "DriverFront": False, + "DriverRear": False, + "PassengerFront": False, + "PassengerRear": False, + "TrunkFront": False, + "TrunkRear": False, + } + }, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + # Reload the entry + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + # Assert the entities restored their values + for entity_id in ( + "cover.test_windows", + "cover.test_charge_port_door", + "cover.test_frunk", + "cover.test_trunk", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-closed") + + # Send some alternative data with everything open + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.FD_WINDOW: "WindowStateOpened", + Signal.FP_WINDOW: "WindowStateOpened", + Signal.RD_WINDOW: "WindowStateOpened", + Signal.RP_WINDOW: "WindowStateOpened", + Signal.CHARGE_PORT_DOOR_OPEN: False, + Signal.DOOR_STATE: { + "DoorState": { + "DriverFront": True, + "DriverRear": True, + "PassengerFront": True, + "PassengerRear": True, + "TrunkFront": True, + "TrunkRear": True, + } + }, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + # Assert the entities get new values + for entity_id in ( + "cover.test_windows", + "cover.test_charge_port_door", + "cover.test_frunk", + "cover.test_trunk", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-open") + + # Send some alternative data with everything unknown + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.FD_WINDOW: "WindowStateUnknown", + Signal.FP_WINDOW: "WindowStateUnknown", + Signal.RD_WINDOW: "WindowStateUnknown", + Signal.RP_WINDOW: "WindowStateUnknown", + Signal.CHARGE_PORT_DOOR_OPEN: None, + Signal.DOOR_STATE: { + "DoorState": { + "DriverFront": None, + "DriverRear": None, + "PassengerFront": None, + "PassengerRear": None, + "TrunkFront": None, + "TrunkRear": None, + } + }, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + # Assert the entities get UNKNOWN values + for entity_id in ( + "cover.test_windows", + "cover.test_charge_port_door", + "cover.test_frunk", + "cover.test_trunk", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-unknown") diff --git a/tests/components/teslemetry/test_device_tracker.py b/tests/components/teslemetry/test_device_tracker.py index d86c3ca8596..38a28092d33 100644 --- a/tests/components/teslemetry/test_device_tracker.py +++ b/tests/components/teslemetry/test_device_tracker.py @@ -2,7 +2,9 @@ from unittest.mock import AsyncMock +import pytest from syrupy.assertion import SnapshotAssertion +from teslemetry_stream.const import Signal from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -12,10 +14,12 @@ from . import assert_entities, assert_entities_alt, setup_platform from .const import VEHICLE_DATA_ALT +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_device_tracker( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_legacy: AsyncMock, ) -> None: """Tests that the device tracker entities are correct.""" @@ -23,14 +27,71 @@ async def test_device_tracker( assert_entities(hass, entry.entry_id, entity_registry, snapshot) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_device_tracker_alt( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, mock_vehicle_data: AsyncMock, + mock_legacy: AsyncMock, ) -> None: """Tests that the device tracker entities are correct.""" mock_vehicle_data.return_value = VEHICLE_DATA_ALT entry = await setup_platform(hass, [Platform.DEVICE_TRACKER]) assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_device_tracker_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the device tracker entities with streaming are correct.""" + + entry = await setup_platform(hass, [Platform.DEVICE_TRACKER]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.LOCATION: { + "latitude": 1.0, + "longitude": 2.0, + }, + Signal.DESTINATION_LOCATION: { + "latitude": 3.0, + "longitude": 4.0, + }, + Signal.DESTINATION_NAME: "Home", + Signal.ORIGIN_LOCATION: None, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + # Assert the entities restored their values + for entity_id in ( + "device_tracker.test_location", + "device_tracker.test_route", + "device_tracker.test_origin", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-state") + + # Reload the entry + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + # Assert the entities restored their values + for entity_id in ( + "device_tracker.test_location", + "device_tracker.test_route", + "device_tracker.test_origin", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-restore") diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 6d4e04c21b4..5481e6cc034 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -142,13 +142,13 @@ async def test_energy_history_refresh_error( async def test_vehicle_stream( hass: HomeAssistant, - mock_listen: AsyncMock, + mock_add_listener: AsyncMock, snapshot: SnapshotAssertion, ) -> None: """Test vehicle stream events.""" - entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) - mock_listen.assert_called_once() + await setup_platform(hass, [Platform.BINARY_SENSOR]) + mock_add_listener.assert_called() state = hass.states.get("binary_sensor.test_status") assert state.state == STATE_ON @@ -156,29 +156,37 @@ async def test_vehicle_stream( state = hass.states.get("binary_sensor.test_user_present") assert state.state == STATE_OFF - runtime_data: TeslemetryData = entry.runtime_data - for listener, _ in runtime_data.vehicles[0].stream._listeners.values(): - listener( - { - "vin": VEHICLE_DATA_ALT["response"]["vin"], - "vehicle_data": VEHICLE_DATA_ALT["response"], - "createdAt": "2024-10-04T10:45:17.537Z", - } - ) + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "vehicle_data": VEHICLE_DATA_ALT["response"], + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_user_present") assert state.state == STATE_ON - for listener, _ in runtime_data.vehicles[0].stream._listeners.values(): - listener( - { - "vin": VEHICLE_DATA_ALT["response"]["vin"], - "state": "offline", - "createdAt": "2024-10-04T10:45:17.537Z", - } - ) + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "state": "offline", + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test_status") assert state.state == STATE_OFF + + +async def test_no_live_status( + hass: HomeAssistant, + mock_live_status: AsyncMock, +) -> None: + """Test coordinator refresh with an error.""" + mock_live_status.side_effect = AsyncMock({"response": ""}) + await setup_platform(hass) + + assert hass.states.get("sensor.energy_site_grid_power") is None diff --git a/tests/components/teslemetry/test_lock.py b/tests/components/teslemetry/test_lock.py index f7c9fea1400..848eee82c39 100644 --- a/tests/components/teslemetry/test_lock.py +++ b/tests/components/teslemetry/test_lock.py @@ -1,9 +1,10 @@ """Test the Teslemetry lock platform.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from syrupy.assertion import SnapshotAssertion +from teslemetry_stream.const import Signal from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -16,14 +17,15 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from . import assert_entities, setup_platform -from .const import COMMAND_OK +from . import assert_entities, reload_platform, setup_platform +from .const import COMMAND_OK, VEHICLE_DATA_ALT async def test_lock( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_legacy: AsyncMock, ) -> None: """Tests that the lock entities are correct.""" @@ -31,6 +33,20 @@ async def test_lock( assert_entities(hass, entry.entry_id, entity_registry, snapshot) +async def test_lock_alt( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_vehicle_data: AsyncMock, + mock_legacy: AsyncMock, +) -> None: + """Tests that the lock entities are correct.""" + + mock_vehicle_data.return_value = VEHICLE_DATA_ALT + entry = await setup_platform(hass, [Platform.LOCK]) + assert_entities(hass, entry.entry_id, entity_registry, snapshot) + + async def test_lock_services( hass: HomeAssistant, ) -> None: @@ -91,3 +107,60 @@ async def test_lock_services( state = hass.states.get(entity_id) assert state.state == LockState.UNLOCKED call.assert_called_once() + + +async def test_lock_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the lock entities with streaming are correct.""" + + entry = await setup_platform(hass, [Platform.LOCK]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.LOCKED: True, + Signal.CHARGE_PORT_LATCH: "ChargePortLatchEngaged", + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + await reload_platform(hass, entry, [Platform.LOCK]) + + # Assert the entities restored their values + for entity_id in ( + "lock.test_lock", + "lock.test_charge_cable_lock", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-locked") + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.LOCKED: False, + Signal.CHARGE_PORT_LATCH: "ChargePortLatchDisengaged", + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + await reload_platform(hass, entry, [Platform.LOCK]) + + # Assert the entities restored their values + for entity_id in ( + "lock.test_lock", + "lock.test_charge_cable_lock", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-unlocked") diff --git a/tests/components/teslemetry/test_number.py b/tests/components/teslemetry/test_number.py index 65c03514d22..95eed5a3f1e 100644 --- a/tests/components/teslemetry/test_number.py +++ b/tests/components/teslemetry/test_number.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest from syrupy.assertion import SnapshotAssertion +from teslemetry_stream import Signal from homeassistant.components.number import ( ATTR_VALUE, @@ -14,7 +15,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import assert_entities, setup_platform +from . import assert_entities, reload_platform, setup_platform from .const import COMMAND_OK, VEHICLE_DATA_ALT @@ -23,6 +24,7 @@ async def test_number( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_legacy: AsyncMock, ) -> None: """Tests that the number entities are correct.""" @@ -100,3 +102,38 @@ async def test_number_services( state = hass.states.get(entity_id) assert state.state == "88" call.assert_called_once() + + +async def test_number_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the number entities with streaming are correct.""" + + entry = await setup_platform(hass, [Platform.NUMBER]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.CHARGE_CURRENT_REQUEST: 24, + Signal.CHARGE_CURRENT_REQUEST_MAX: 32, + Signal.CHARGE_LIMIT_SOC: 99, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + await reload_platform(hass, entry, [Platform.NUMBER]) + + # Assert the entities restored their values + for entity_id in ( + "number.test_charge_current", + "number.test_charge_limit", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-state") diff --git a/tests/components/teslemetry/test_select.py b/tests/components/teslemetry/test_select.py index 005a6a2004e..c49e83803cd 100644 --- a/tests/components/teslemetry/test_select.py +++ b/tests/components/teslemetry/test_select.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch import pytest from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.const import EnergyExportMode, EnergyOperationMode +from teslemetry_stream.const import Signal from homeassistant.components.select import ( ATTR_OPTION, @@ -16,7 +17,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import assert_entities, setup_platform +from . import assert_entities, reload_platform, setup_platform from .const import COMMAND_OK, VEHICLE_DATA_ALT @@ -25,6 +26,7 @@ async def test_select( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + mock_legacy: AsyncMock, ) -> None: """Tests that the select entities are correct.""" @@ -106,6 +108,7 @@ async def test_select_invalid_data( snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, mock_vehicle_data: AsyncMock, + mock_legacy: AsyncMock, ) -> None: """Tests that the select entities handle invalid data.""" @@ -119,3 +122,45 @@ async def test_select_invalid_data( assert state.state == STATE_UNKNOWN state = hass.states.get("select.test_steering_wheel_heater") assert state.state == STATE_UNKNOWN + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the select entities with streaming are correct.""" + + entry = await setup_platform(hass, [Platform.SELECT]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.SEAT_HEATER_LEFT: 0, + Signal.SEAT_HEATER_RIGHT: 1, + Signal.SEAT_HEATER_REAR_LEFT: 2, + Signal.SEAT_HEATER_REAR_RIGHT: 3, + Signal.HVAC_STEERING_WHEEL_HEAT_LEVEL: 0, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + await reload_platform(hass, entry, [Platform.SELECT]) + + # Assert the entities restored their values + for entity_id in ( + "select.test_seat_heater_front_left", + "select.test_seat_heater_front_right", + "select.test_seat_heater_rear_left", + "select.test_seat_heater_rear_center", + "select.test_seat_heater_rear_right", + "select.test_steering_wheel_heater", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=entity_id) diff --git a/tests/components/teslemetry/test_sensor.py b/tests/components/teslemetry/test_sensor.py index f0b472a7183..a488ebc8a06 100644 --- a/tests/components/teslemetry/test_sensor.py +++ b/tests/components/teslemetry/test_sensor.py @@ -1,10 +1,11 @@ """Test the Teslemetry sensor platform.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from teslemetry_stream import Signal from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL from homeassistant.const import Platform @@ -25,11 +26,15 @@ async def test_sensors( freezer: FrozenDateTimeFactory, mock_vehicle_data: AsyncMock, ) -> None: - """Tests that the sensor entities are correct.""" + """Tests that the sensor entities with the legacy polling are correct.""" freezer.move_to("2024-01-01 00:00:00+00:00") - entry = await setup_platform(hass, [Platform.SENSOR]) + # Force the vehicle to use polling + with patch( + "homeassistant.components.teslemetry.VehicleSpecific.pre2021", return_value=True + ): + entry = await setup_platform(hass, [Platform.SENSOR]) assert_entities(hass, entry.entry_id, entity_registry, snapshot) @@ -40,3 +45,54 @@ async def test_sensors( await hass.async_block_till_done() assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors_streaming( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + mock_vehicle_data: AsyncMock, + mock_add_listener: AsyncMock, +) -> None: + """Tests that the sensor entities with streaming are correct.""" + + freezer.move_to("2024-01-01 00:00:00+00:00") + + entry = await setup_platform(hass, [Platform.SENSOR]) + + # Stream update + mock_add_listener.send( + { + "vin": VEHICLE_DATA_ALT["response"]["vin"], + "data": { + Signal.DETAILED_CHARGE_STATE: "DetailedChargeStateCharging", + Signal.BATTERY_LEVEL: 90, + Signal.AC_CHARGING_ENERGY_IN: 10, + Signal.AC_CHARGING_POWER: 2, + Signal.CHARGING_CABLE_TYPE: None, + Signal.TIME_TO_FULL_CHARGE: 10, + Signal.MINUTES_TO_ARRIVAL: None, + }, + "createdAt": "2024-10-04T10:45:17.537Z", + } + ) + await hass.async_block_till_done() + + # Reload the entry + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + # Assert the entities restored their values + for entity_id in ( + "sensor.test_charging", + "sensor.test_battery_level", + "sensor.test_charge_energy_added", + "sensor.test_charger_power", + "sensor.test_charge_cable", + "sensor.test_time_to_full_charge", + "sensor.test_time_to_arrival", + ): + state = hass.states.get(entity_id) + assert state.state == snapshot(name=f"{entity_id}-state") diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensor.ambr similarity index 97% rename from tests/components/tessie/snapshots/test_binary_sensors.ambr rename to tests/components/tessie/snapshots/test_binary_sensor.ambr index 6c0da044df2..2fe97b88811 100644 --- a/tests/components/tessie/snapshots/test_binary_sensors.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -282,6 +288,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -328,6 +335,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -375,6 +383,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -422,6 +431,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -469,6 +479,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -516,6 +527,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -563,6 +575,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -610,6 +623,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -657,6 +671,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -704,6 +719,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -751,6 +767,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -798,6 +815,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -844,6 +862,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -891,6 +910,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -938,6 +958,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -985,6 +1006,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1032,6 +1054,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1078,6 +1101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1125,6 +1149,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1172,6 +1197,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1219,6 +1245,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1266,6 +1293,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1313,6 +1341,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1359,6 +1388,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_button.ambr b/tests/components/tessie/snapshots/test_button.ambr index 7757d1f2fea..96ece94a1c9 100644 --- a/tests/components/tessie/snapshots/test_button.ambr +++ b/tests/components/tessie/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_climate.ambr b/tests/components/tessie/snapshots/test_climate.ambr index 959b42cea53..415988e783e 100644 --- a/tests/components/tessie/snapshots/test_climate.ambr +++ b/tests/components/tessie/snapshots/test_climate.ambr @@ -19,6 +19,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_cover.ambr b/tests/components/tessie/snapshots/test_cover.ambr index 6338758afb7..fdf2a967048 100644 --- a/tests/components/tessie/snapshots/test_cover.ambr +++ b/tests/components/tessie/snapshots/test_cover.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_device_tracker.ambr b/tests/components/tessie/snapshots/test_device_tracker.ambr index 61f89db8637..92502340aa2 100644 --- a/tests/components/tessie/snapshots/test_device_tracker.ambr +++ b/tests/components/tessie/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index cea2bebbddb..f819281d79b 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index 6c355c8ddca..911598004a6 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -7,6 +7,7 @@ 'capabilities': dict({ }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_number.ambr b/tests/components/tessie/snapshots/test_number.ambr index 6e641bdf5b7..0e43695ca78 100644 --- a/tests/components/tessie/snapshots/test_number.ambr +++ b/tests/components/tessie/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -127,6 +129,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -184,6 +187,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +245,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_select.ambr b/tests/components/tessie/snapshots/test_select.ambr index acc1946aab5..f118633aded 100644 --- a/tests/components/tessie/snapshots/test_select.ambr +++ b/tests/components/tessie/snapshots/test_select.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -127,6 +129,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -186,6 +189,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -245,6 +249,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -304,6 +309,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -363,6 +369,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -422,6 +429,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -481,6 +489,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index 0a5ff4603aa..5465f89d808 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -122,6 +124,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -179,6 +182,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +240,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -293,6 +298,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -350,6 +356,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -404,6 +411,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -461,6 +469,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -516,6 +525,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -566,6 +576,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -617,6 +628,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -674,6 +686,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -731,6 +744,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -788,6 +802,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -842,6 +857,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -896,6 +912,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -947,6 +964,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -998,6 +1016,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1056,6 +1075,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1111,6 +1131,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1159,6 +1180,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1213,6 +1235,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1267,6 +1290,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1321,6 +1345,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1378,6 +1403,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1432,6 +1458,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1486,6 +1513,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1542,6 +1570,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1597,6 +1626,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1651,6 +1681,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1700,6 +1731,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1747,6 +1779,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1796,6 +1829,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1853,6 +1887,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1910,6 +1945,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1967,6 +2003,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2024,6 +2061,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2075,6 +2113,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2132,6 +2171,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2200,6 +2240,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2272,6 +2313,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2331,6 +2373,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2377,6 +2420,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/snapshots/test_switch.ambr b/tests/components/tessie/snapshots/test_switch.ambr index 3b7a3623de8..371ef822122 100644 --- a/tests/components/tessie/snapshots/test_switch.ambr +++ b/tests/components/tessie/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -119,7 +122,7 @@ 'platform': 'tessie', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'charge_state_charge_enable_request', + 'translation_key': 'charge_state_charging_state', 'unique_id': 'VINVINVIN-charge_state_charge_enable_request', 'unit_of_measurement': None, }) @@ -145,6 +148,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -192,6 +196,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -239,6 +244,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -286,6 +292,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -351,6 +358,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- diff --git a/tests/components/tessie/snapshots/test_update.ambr b/tests/components/tessie/snapshots/test_update.ambr index 1728c13b0ad..e4c25e2230f 100644 --- a/tests/components/tessie/snapshots/test_update.ambr +++ b/tests/components/tessie/snapshots/test_update.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tessie/test_binary_sensors.py b/tests/components/tessie/test_binary_sensor.py similarity index 100% rename from tests/components/tessie/test_binary_sensors.py rename to tests/components/tessie/test_binary_sensor.py diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py index 49a53fd327c..02a8f22b6ea 100644 --- a/tests/components/tessie/test_cover.py +++ b/tests/components/tessie/test_cover.py @@ -112,4 +112,4 @@ async def test_errors(hass: HomeAssistant) -> None: blocking=True, ) mock_set.assert_called_once() - assert str(error.value) == f"Command failed, {TEST_RESPONSE_ERROR["reason"]}" + assert str(error.value) == f"Command failed, {TEST_RESPONSE_ERROR['reason']}" diff --git a/tests/components/tessie/test_switch.py b/tests/components/tessie/test_switch.py index 499e529b2e8..690ad7d1ab4 100644 --- a/tests/components/tessie/test_switch.py +++ b/tests/components/tessie/test_switch.py @@ -20,7 +20,7 @@ from .common import RESPONSE_OK, assert_entities, setup_platform async def test_switches( hass: HomeAssistant, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry ) -> None: - """Tests that the switche entities are correct.""" + """Tests that the switch entities are correct.""" entry = await setup_platform(hass, [Platform.SWITCH]) diff --git a/tests/components/thermopro/test_config_flow.py b/tests/components/thermopro/test_config_flow.py index 9b9fdd67334..3cf68fb612c 100644 --- a/tests/components/thermopro/test_config_flow.py +++ b/tests/components/thermopro/test_config_flow.py @@ -79,6 +79,38 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" +async def test_async_step_user_replace_ignored(hass: HomeAssistant) -> None: + """Test setup from service info and replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TP357_SERVICE_INFO.address, + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.thermopro.config_flow.async_discovered_service_info", + return_value=[TP357_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.thermopro.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "TP357 (2142) AC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py index c31a1937d45..7feefdafedf 100644 --- a/tests/components/thread/test_config_flow.py +++ b/tests/components/thread/test_config_flow.py @@ -3,11 +3,12 @@ from ipaddress import ip_address from unittest.mock import patch -from homeassistant.components import thread, zeroconf +from homeassistant.components import thread from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -TEST_ZEROCONF_RECORD = zeroconf.ZeroconfServiceInfo( +TEST_ZEROCONF_RECORD = ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="HomeAssistant OpenThreadBorderRouter #0BBF", diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py index d817c9612aa..845df86a88c 100644 --- a/tests/components/tibber/test_statistics.py +++ b/tests/components/tibber/test_statistics.py @@ -10,10 +10,13 @@ from homeassistant.util import dt as dt_util from .test_common import CONSUMPTION_DATA_1, PRODUCTION_DATA_1, mock_get_homes +from tests.common import MockConfigEntry from tests.components.recorder.common import async_wait_recording_done -async def test_async_setup_entry(recorder_mock: Recorder, hass: HomeAssistant) -> None: +async def test_async_setup_entry( + recorder_mock: Recorder, hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Test setup Tibber.""" tibber_connection = AsyncMock() tibber_connection.name = "tibber" @@ -21,7 +24,7 @@ async def test_async_setup_entry(recorder_mock: Recorder, hass: HomeAssistant) - tibber_connection.fetch_production_data_active_homes.return_value = None tibber_connection.get_homes = mock_get_homes - coordinator = TibberDataCoordinator(hass, tibber_connection) + coordinator = TibberDataCoordinator(hass, config_entry, tibber_connection) await coordinator._async_update_data() await async_wait_recording_done(hass) diff --git a/tests/components/tile/__init__.py b/tests/components/tile/__init__.py index 5f26eb01ce0..d459dbad187 100644 --- a/tests/components/tile/__init__.py +++ b/tests/components/tile/__init__.py @@ -1 +1,13 @@ -"""Define tests for the Tile component.""" +"""Tests for the Tile integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py index 01a711d9261..4391853c878 100644 --- a/tests/components/tile/conftest.py +++ b/tests/components/tile/conftest.py @@ -1,77 +1,88 @@ """Define test fixtures for Tile.""" from collections.abc import Generator -import json -from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from datetime import datetime +from unittest.mock import AsyncMock, patch import pytest +from pytile.api import API from pytile.tile import Tile from homeassistant.components.tile.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from .const import TEST_PASSWORD, TEST_USERNAME -TEST_PASSWORD = "123abc" -TEST_USERNAME = "user@host.com" +from tests.common import MockConfigEntry -@pytest.fixture(name="api") -def api_fixture(data_tile_details: dict[str, Any]) -> Mock: - """Define a pytile API object.""" - tile = Tile(None, data_tile_details) - tile.async_update = AsyncMock() +@pytest.fixture +def tile() -> AsyncMock: + """Define a Tile object.""" + mock = AsyncMock(spec=Tile) + mock.uuid = "19264d2dffdbca32" + mock.name = "Wallet" + mock.dead = False + mock.latitude = 1 + mock.longitude = 1 + mock.altitude = 0 + mock.lost = False + mock.last_timestamp = datetime(2020, 8, 12, 17, 55, 26) + mock.lost_timestamp = datetime(1969, 12, 31, 19, 0, 0) + mock.ring_state = "STOPPED" + mock.voip_state = "OFFLINE" + mock.hardware_version = "02.09" + mock.firmware_version = "01.12.14.0" + mock.as_dict.return_value = { + "accuracy": 13.496111, + "altitude": 0, + "archetype": "WALLET", + "dead": False, + "firmware_version": "01.12.14.0", + "hardware_version": "02.09", + "kind": "TILE", + "last_timestamp": datetime(2020, 8, 12, 17, 55, 26), + "latitude": 0, + "longitude": 0, + "lost": False, + "lost_timestamp": datetime(1969, 12, 31, 19, 0, 0), + "name": "Wallet", + "ring_state": "STOPPED", + "uuid": "19264d2dffdbca32", + "visible": True, + "voip_state": "OFFLINE", + } + return mock - return Mock( - async_get_tiles=AsyncMock( - return_value={data_tile_details["result"]["tile_uuid"]: tile} - ) + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Define a config entry fixture.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USERNAME, + data={CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD}, ) -@pytest.fixture(name="config_entry") -def config_entry_fixture( - hass: HomeAssistant, config: dict[str, Any] -) -> MockConfigEntry: - """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) - entry.add_to_hass(hass) - return entry - - -@pytest.fixture(name="config") -def config_fixture() -> dict[str, Any]: - """Define a config entry data fixture.""" - return { - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - } - - -@pytest.fixture(name="data_tile_details", scope="package") -def data_tile_details_fixture(): - """Define a Tile details data payload.""" - return json.loads(load_fixture("tile_details_data.json", "tile")) - - -@pytest.fixture(name="mock_pytile") -def mock_pytile_fixture(api: Mock) -> Generator[None]: +@pytest.fixture +def mock_pytile(tile: AsyncMock) -> Generator[None]: """Define a fixture to patch pytile.""" + client = AsyncMock(spec=API) + client.async_get_tiles = AsyncMock(return_value={"19264d2dffdbca32": tile}) with ( patch( - "homeassistant.components.tile.config_flow.async_login", return_value=api + "homeassistant.components.tile.config_flow.async_login", return_value=client ), - patch("homeassistant.components.tile.async_login", return_value=api), + patch("homeassistant.components.tile.async_login", return_value=client), ): yield -@pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture( - hass: HomeAssistant, config_entry: MockConfigEntry, mock_pytile: None -) -> None: - """Define a fixture to set up tile.""" - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() +@pytest.fixture +def mock_setup_entry(): + """Mock async_setup_entry.""" + with patch( + "homeassistant.components.tile.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/tile/const.py b/tests/components/tile/const.py new file mode 100644 index 00000000000..c48a0e73dab --- /dev/null +++ b/tests/components/tile/const.py @@ -0,0 +1,4 @@ +"""Constants for the Tile component tests.""" + +TEST_PASSWORD = "123abc" +TEST_USERNAME = "user@host.com" diff --git a/tests/components/tile/snapshots/test_binary_sensor.ambr b/tests/components/tile/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6de356ebf51 --- /dev/null +++ b/tests/components/tile/snapshots/test_binary_sensor.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.wallet_lost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.wallet_lost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lost', + 'platform': 'tile', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lost', + 'unique_id': 'user@host.com_19264d2dffdbca32_lost', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.wallet_lost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wallet Lost', + }), + 'context': , + 'entity_id': 'binary_sensor.wallet_lost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/tile/snapshots/test_device_tracker.ambr b/tests/components/tile/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..f5de1511c99 --- /dev/null +++ b/tests/components/tile/snapshots/test_device_tracker.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_all_entities[device_tracker.wallet-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.wallet', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tile', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tile', + 'unique_id': 'user@host.com_19264d2dffdbca32', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[device_tracker.wallet-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'altitude': 0, + 'friendly_name': 'Wallet', + 'gps_accuracy': 1, + 'is_lost': False, + 'last_lost_timestamp': datetime.datetime(1970, 1, 1, 3, 0, tzinfo=datetime.timezone.utc), + 'last_timestamp': datetime.datetime(2020, 8, 13, 0, 55, 26, tzinfo=datetime.timezone.utc), + 'latitude': 1, + 'longitude': 1, + 'ring_state': 'STOPPED', + 'source_type': , + 'voip_state': 'OFFLINE', + }), + 'context': , + 'entity_id': 'device_tracker.wallet', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/tile/snapshots/test_diagnostics.ambr b/tests/components/tile/snapshots/test_diagnostics.ambr index c04bd93315f..2876b058408 100644 --- a/tests/components/tile/snapshots/test_diagnostics.ambr +++ b/tests/components/tile/snapshots/test_diagnostics.ambr @@ -14,7 +14,7 @@ 'latitude': '**REDACTED**', 'longitude': '**REDACTED**', 'lost': False, - 'lost_timestamp': '1969-12-31T23:59:59.999000', + 'lost_timestamp': '1969-12-31T19:00:00', 'name': 'Wallet', 'ring_state': 'STOPPED', 'uuid': '**REDACTED**', diff --git a/tests/components/tile/snapshots/test_init.ambr b/tests/components/tile/snapshots/test_init.ambr new file mode 100644 index 00000000000..ffdf6a6251a --- /dev/null +++ b/tests/components/tile/snapshots/test_init.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '02.09', + 'id': , + 'identifiers': set({ + tuple( + 'tile', + '19264d2dffdbca32', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Tile Inc.', + 'model': None, + 'model_id': None, + 'name': 'Wallet', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '01.12.14.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tile/test_binary_sensor.py b/tests/components/tile/test_binary_sensor.py new file mode 100644 index 00000000000..c8b4b9b8376 --- /dev/null +++ b/tests/components/tile/test_binary_sensor.py @@ -0,0 +1,27 @@ +"""Tests for the Tile Binary sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_pytile: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.tile.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 849be41d560..2ebfaa5c583 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, patch import pytest from pytile.errors import InvalidAuthError, TileError -from homeassistant.components.tile import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.components.tile.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -16,15 +16,45 @@ from .conftest import TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry +async def test_full_flow( + hass: HomeAssistant, mock_pytile: AsyncMock, mock_setup_entry: AsyncMock +) -> None: + """Test a full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + assert result["result"].unique_id == TEST_USERNAME + + @pytest.mark.parametrize( - ("mock_login_response", "errors"), + ("exception", "errors"), [ - (AsyncMock(side_effect=InvalidAuthError), {"base": "invalid_auth"}), - (AsyncMock(side_effect=TileError), {"base": "unknown"}), + (InvalidAuthError, {"base": "invalid_auth"}), + (TileError, {"base": "unknown"}), ], ) async def test_create_entry( - hass: HomeAssistant, api, config, errors, mock_login_response, mock_pytile + hass: HomeAssistant, + mock_pytile: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + errors: dict[str, str], ) -> None: """Test creating an entry.""" result = await hass.config_entries.flow.async_init( @@ -33,59 +63,61 @@ async def test_create_entry( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - # Test errors that can arise: with patch( - "homeassistant.components.tile.config_flow.async_login", mock_login_response + "homeassistant.components.tile.config_flow.async_login", side_effect=exception ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config + result["flow_id"], + user_input={ + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors - # Test that we can recover from login errors: result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config + result["flow_id"], + user_input={ + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == TEST_USERNAME - assert result["data"] == { - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - } -async def test_duplicate_error(hass: HomeAssistant, config, setup_config_entry) -> None: +async def test_duplicate_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test that errors are shown when duplicates are added.""" + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_import_entry(hass: HomeAssistant, config, mock_pytile) -> None: - """Test importing an entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == TEST_USERNAME - assert result["data"] == { - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - } - - async def test_step_reauth( - hass: HomeAssistant, config, config_entry: MockConfigEntry, setup_config_entry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, + mock_pytile: AsyncMock, ) -> None: """Test that the reauth step works.""" - result = await config_entry.start_reauth_flow(hass) - assert result["step_id"] == "reauth_confirm" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -95,3 +127,45 @@ async def test_step_reauth( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("exception", "errors"), + [ + (InvalidAuthError, {"base": "invalid_auth"}), + (TileError, {"base": "unknown"}), + ], +) +async def test_step_reauth_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, + mock_pytile: AsyncMock, + exception: Exception, + errors: dict[str, str], +) -> None: + """Test that the reauth step can recover from an error.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.tile.config_flow.async_login", side_effect=exception + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == errors + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password"} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/tile/test_device_tracker.py b/tests/components/tile/test_device_tracker.py new file mode 100644 index 00000000000..105cae1a7d7 --- /dev/null +++ b/tests/components/tile/test_device_tracker.py @@ -0,0 +1,27 @@ +"""Tests for the Tile Device tracker platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_pytile: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.tile.PLATFORMS", [Platform.DEVICE_TRACKER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/tile/test_diagnostics.py b/tests/components/tile/test_diagnostics.py index 2ac3e06ccd8..87bc670d604 100644 --- a/tests/components/tile/test_diagnostics.py +++ b/tests/components/tile/test_diagnostics.py @@ -1,22 +1,28 @@ """Test Tile diagnostics.""" +from unittest.mock import AsyncMock + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant +from . import setup_integration + +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, - config_entry, hass_client: ClientSessionGenerator, - setup_config_entry, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_pytile: AsyncMock, ) -> None: """Test config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) == snapshot ) diff --git a/tests/components/tile/test_init.py b/tests/components/tile/test_init.py new file mode 100644 index 00000000000..fba354ade17 --- /dev/null +++ b/tests/components/tile/test_init.py @@ -0,0 +1,29 @@ +"""Tests for the Tile integration.""" + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion + +from homeassistant.components.tile.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_pytile: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, "19264d2dffdbca32")} + ) + assert device_entry is not None + assert device_entry == snapshot diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py index fd996228034..9c9450f3996 100644 --- a/tests/components/tilt_ble/test_config_flow.py +++ b/tests/components/tilt_ble/test_config_flow.py @@ -79,6 +79,37 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" +async def test_async_step_user_replaces_ignored(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TILT_GREEN_SERVICE_INFO.address, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.tilt_ble.config_flow.async_discovered_service_info", + return_value=[TILT_GREEN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.tilt_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F6:0F:28:F2:1F:CB"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Tilt Green" + assert result2["data"] == {} + assert result2["result"].unique_id == "F6:0F:28:F2:1F:CB" + + async def test_async_step_user_device_added_between_steps(hass: HomeAssistant) -> None: """Test the device gets added via another flow between steps.""" with patch( diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index ddeec48b3d2..3daa0314cbd 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.time_date.const import OPTION_TYPES from homeassistant.core import HomeAssistant from homeassistant.helpers import event -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import load_int diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 95baa07eaa9..6e68b354087 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -196,6 +196,12 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: "event": EVENT_TIMER_CANCELLED, "data": {}, }, + { + "call": SERVICE_CANCEL, + "state": STATUS_IDLE, + "event": None, + "data": {}, + }, { "call": SERVICE_START, "state": STATUS_ACTIVE, @@ -208,6 +214,12 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: "event": EVENT_TIMER_FINISHED, "data": {}, }, + { + "call": SERVICE_FINISH, + "state": STATUS_IDLE, + "event": None, + "data": {}, + }, { "call": SERVICE_START, "state": STATUS_ACTIVE, @@ -244,6 +256,18 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: "event": EVENT_TIMER_RESTARTED, "data": {}, }, + { + "call": SERVICE_PAUSE, + "state": STATUS_PAUSED, + "event": EVENT_TIMER_PAUSED, + "data": {}, + }, + { + "call": SERVICE_FINISH, + "state": STATUS_IDLE, + "event": EVENT_TIMER_FINISHED, + "data": {}, + }, ] expected_events = 0 diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index b4b6b13d8e3..8b9a81d7542 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -7,10 +7,10 @@ import pytest from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_next from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/todo/__init__.py b/tests/components/todo/__init__.py index 0138e561fad..53772ab144e 100644 --- a/tests/components/todo/__init__.py +++ b/tests/components/todo/__init__.py @@ -3,7 +3,7 @@ from homeassistant.components.todo import DOMAIN, TodoItem, TodoListEntity from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import MockConfigEntry, MockPlatform, mock_platform @@ -44,7 +44,7 @@ async def create_mock_platform( async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test event platform via config entry.""" async_add_entities(entities) diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 4b2bfea2e30..84f0fa740e9 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -70,6 +70,7 @@ def make_api_task( section_id=None, url="https://todoist.com", sync_id=None, + duration=None, ) @@ -94,6 +95,7 @@ def mock_api(tasks: list[Task]) -> AsyncMock: url="", is_inbox_project=False, is_team_inbox=False, + can_assign_tasks=False, order=1, parent_id=None, view_style="list", diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 73382944cf0..e918edf70a4 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -5,14 +5,14 @@ from unittest.mock import Mock, patch import pytest from tololib import ToloCommunicationError -from homeassistant.components import dhcp from homeassistant.components.tolo.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( +MOCK_DHCP_DATA = DhcpServiceInfo( ip="127.0.0.2", macaddress="001122334455", hostname="mock_hostname" ) diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index f50d999548f..e4f08f55dba 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -8,7 +8,7 @@ import requests_mock import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -import homeassistant.components.tomato.device_tracker as tomato +from homeassistant.components.tomato import device_tracker as tomato from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index 828cad71e07..34d451ec0b8 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -49,20 +49,15 @@ USER = { "UserFeatureList": "Master=0,User Administration=0,Configuration Administration=0", } -RESPONSE_AUTHENTICATE = { +RESPONSE_SESSION_DETAILS = { "ResultCode": ResultCode.SUCCESS.value, - "SessionID": 1, + "ResultData": "Success", + "SessionID": "12345", "Locations": LOCATIONS, "ModuleFlags": MODULE_FLAGS, "UserInfo": USER, } -RESPONSE_AUTHENTICATE_FAILED = { - "ResultCode": ResultCode.BAD_USER_OR_PASSWORD.value, - "ResultData": "test bad authentication", -} - - PARTITION_DISARMED = { "PartitionID": "1", "ArmingState": ArmingState.DISARMED, @@ -359,13 +354,13 @@ OPTIONS_DATA = {AUTO_BYPASS: False, CODE_REQUIRED: False} OPTIONS_DATA_CODE_REQUIRED = {AUTO_BYPASS: False, CODE_REQUIRED: True} PARTITION_DETAILS_1 = { - "PartitionID": 1, + "PartitionID": "1", "ArmingState": ArmingState.DISARMED.value, "PartitionName": "Test1", } PARTITION_DETAILS_2 = { - "PartitionID": 2, + "PartitionID": "2", "ArmingState": ArmingState.DISARMED.value, "PartitionName": "Test2", } @@ -402,6 +397,12 @@ RESPONSE_GET_ZONE_DETAILS_SUCCESS = { TOTALCONNECT_REQUEST = ( "homeassistant.components.totalconnect.TotalConnectClient.request" ) +TOTALCONNECT_GET_CONFIG = ( + "homeassistant.components.totalconnect.TotalConnectClient._get_configuration" +) +TOTALCONNECT_REQUEST_TOKEN = ( + "homeassistant.components.totalconnect.TotalConnectClient._request_token" +) async def setup_platform( @@ -420,7 +421,7 @@ async def setup_platform( mock_entry.add_to_hass(hass) responses = [ - RESPONSE_AUTHENTICATE, + RESPONSE_SESSION_DETAILS, RESPONSE_PARTITION_DETAILS, RESPONSE_GET_ZONE_DETAILS_SUCCESS, RESPONSE_DISARMED, @@ -433,6 +434,8 @@ async def setup_platform( TOTALCONNECT_REQUEST, side_effect=responses, ) as mock_request, + patch(TOTALCONNECT_GET_CONFIG, side_effect=None), + patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), ): assert await async_setup_component(hass, DOMAIN, {}) assert mock_request.call_count == 5 @@ -448,17 +451,21 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: mock_entry.add_to_hass(hass) responses = [ - RESPONSE_AUTHENTICATE, + RESPONSE_SESSION_DETAILS, RESPONSE_PARTITION_DETAILS, RESPONSE_GET_ZONE_DETAILS_SUCCESS, RESPONSE_DISARMED, RESPONSE_DISARMED, ] - with patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request: + with ( + patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request, + patch(TOTALCONNECT_GET_CONFIG, side_effect=None), + patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), + ): await hass.config_entries.async_setup(mock_entry.entry_id) assert mock_request.call_count == 5 await hass.async_block_till_done() diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr index ef7cb386b33..a63319a6c76 100644 --- a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -64,6 +65,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr index 1eccff1dfc3..ac79455a0d5 100644 --- a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr +++ b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -56,6 +57,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -106,6 +108,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -156,6 +159,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -206,6 +210,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -256,6 +261,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -306,6 +312,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -356,6 +363,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -406,6 +414,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -456,6 +465,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -506,6 +516,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -556,6 +567,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -606,6 +618,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -656,6 +669,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -706,6 +720,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -756,6 +771,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -806,6 +822,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -854,6 +871,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -902,6 +920,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -949,6 +968,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -997,6 +1017,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1045,6 +1066,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1093,6 +1115,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1143,6 +1166,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1193,6 +1217,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/totalconnect/snapshots/test_button.ambr b/tests/components/totalconnect/snapshots/test_button.ambr index af3318591c6..96d38567236 100644 --- a/tests/components/totalconnect/snapshots/test_button.ambr +++ b/tests/components/totalconnect/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 86419bff817..f5020394bce 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -18,13 +18,15 @@ from homeassistant.data_entry_flow import FlowResultType from .common import ( CONFIG_DATA, CONFIG_DATA_NO_USERCODES, - RESPONSE_AUTHENTICATE, RESPONSE_DISARMED, RESPONSE_GET_ZONE_DETAILS_SUCCESS, RESPONSE_PARTITION_DETAILS, + RESPONSE_SESSION_DETAILS, RESPONSE_SUCCESS, RESPONSE_USER_CODE_INVALID, + TOTALCONNECT_GET_CONFIG, TOTALCONNECT_REQUEST, + TOTALCONNECT_REQUEST_TOKEN, USERNAME, ) @@ -48,7 +50,7 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: """Test user locations form.""" # user/pass provided, so check if valid then ask for usercodes on locations form responses = [ - RESPONSE_AUTHENTICATE, + RESPONSE_SESSION_DETAILS, RESPONSE_PARTITION_DETAILS, RESPONSE_GET_ZONE_DETAILS_SUCCESS, RESPONSE_DISARMED, @@ -61,6 +63,8 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: TOTALCONNECT_REQUEST, side_effect=responses, ) as mock_request, + patch(TOTALCONNECT_GET_CONFIG, side_effect=None), + patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), patch( "homeassistant.components.totalconnect.async_setup_entry", return_value=True ), @@ -180,7 +184,7 @@ async def test_reauth(hass: HomeAssistant) -> None: async def test_no_locations(hass: HomeAssistant) -> None: """Test with no user locations.""" responses = [ - RESPONSE_AUTHENTICATE, + RESPONSE_SESSION_DETAILS, RESPONSE_PARTITION_DETAILS, RESPONSE_GET_ZONE_DETAILS_SUCCESS, RESPONSE_DISARMED, @@ -191,6 +195,8 @@ async def test_no_locations(hass: HomeAssistant) -> None: TOTALCONNECT_REQUEST, side_effect=responses, ) as mock_request, + patch(TOTALCONNECT_GET_CONFIG, side_effect=None), + patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), patch( "homeassistant.components.totalconnect.async_setup_entry", return_value=True ), @@ -221,7 +227,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) responses = [ - RESPONSE_AUTHENTICATE, + RESPONSE_SESSION_DETAILS, RESPONSE_PARTITION_DETAILS, RESPONSE_GET_ZONE_DETAILS_SUCCESS, RESPONSE_DISARMED, @@ -229,7 +235,11 @@ async def test_options_flow(hass: HomeAssistant) -> None: RESPONSE_DISARMED, ] - with patch(TOTALCONNECT_REQUEST, side_effect=responses): + with ( + patch(TOTALCONNECT_REQUEST, side_effect=responses), + patch(TOTALCONNECT_GET_CONFIG, side_effect=None), + patch(TOTALCONNECT_REQUEST_TOKEN, side_effect=None), + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 809ab3bfd78..ac5bb347765 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -1,125 +1,51 @@ """Tests for the TP-Link component.""" from collections import namedtuple -from datetime import datetime +from dataclasses import replace +from datetime import datetime, timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from kasa import ( BaseProtocol, Device, - DeviceConfig, - DeviceConnectionParameters, - DeviceEncryptionType, - DeviceFamily, DeviceType, Feature, KasaException, Module, + ThermostatState, ) -from kasa.interfaces import Fan, Light, LightEffect, LightState +from kasa.interfaces import Fan, Light, LightEffect, LightState, Thermostat +from kasa.smart.modules import Speaker from kasa.smart.modules.alarm import Alarm +from kasa.smart.modules.clean import AreaUnit, Clean, ErrorCode, Status +from kasa.smartcam.modules.camera import LOCAL_STREAMING_PORT, Camera from syrupy import SnapshotAssertion from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN -from homeassistant.components.tplink import ( - CONF_AES_KEYS, - CONF_ALIAS, - CONF_CONNECTION_PARAMETERS, - CONF_CREDENTIALS_HASH, - CONF_HOST, - CONF_MODEL, - CONF_USES_HTTP, - Credentials, -) from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import UNDEFINED from homeassistant.setup import async_setup_component +from .const import ( + ALIAS, + CREDENTIALS_HASH_LEGACY, + DEVICE_CONFIG_LEGACY, + DEVICE_ID, + IP_ADDRESS, + MAC_ADDRESS, + MODEL, +) + from tests.common import MockConfigEntry, load_json_value_fixture ColorTempRange = namedtuple("ColorTempRange", ["min", "max"]) # noqa: PYI024 -MODULE = "homeassistant.components.tplink" -MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" -IP_ADDRESS = "127.0.0.1" -IP_ADDRESS2 = "127.0.0.2" -ALIAS = "My Bulb" -MODEL = "HS100" -MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" -DEVICE_ID = "123456789ABCDEFGH" -DEVICE_ID_MAC = "AA:BB:CC:DD:EE:FF" -DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") -MAC_ADDRESS2 = "11:22:33:44:55:66" -DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" -CREDENTIALS_HASH_LEGACY = "" -CONN_PARAMS_LEGACY = DeviceConnectionParameters( - DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor -) -DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) -DEVICE_CONFIG_DICT_LEGACY = { - k: v for k, v in DEVICE_CONFIG_LEGACY.to_dict().items() if k != "credentials" -} -CREDENTIALS = Credentials("foo", "bar") -CREDENTIALS_HASH_AES = "AES/abcdefghijklmnopqrstuvabcdefghijklmnopqrstuv==" -CREDENTIALS_HASH_KLAP = "KLAP/abcdefghijklmnopqrstuv==" -CONN_PARAMS_KLAP = DeviceConnectionParameters( - DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap -) -DEVICE_CONFIG_KLAP = DeviceConfig( - IP_ADDRESS, - credentials=CREDENTIALS, - connection_type=CONN_PARAMS_KLAP, - uses_http=True, -) -CONN_PARAMS_AES = DeviceConnectionParameters( - DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes -) -AES_KEYS = {"private": "foo", "public": "bar"} -DEVICE_CONFIG_AES = DeviceConfig( - IP_ADDRESS2, - credentials=CREDENTIALS, - connection_type=CONN_PARAMS_AES, - uses_http=True, - aes_keys=AES_KEYS, -) -DEVICE_CONFIG_DICT_KLAP = { - k: v for k, v in DEVICE_CONFIG_KLAP.to_dict().items() if k != "credentials" -} -DEVICE_CONFIG_DICT_AES = { - k: v for k, v in DEVICE_CONFIG_AES.to_dict().items() if k != "credentials" -} -CREATE_ENTRY_DATA_LEGACY = { - CONF_HOST: IP_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_MODEL: MODEL, - CONF_CONNECTION_PARAMETERS: CONN_PARAMS_LEGACY.to_dict(), - CONF_USES_HTTP: False, -} - -CREATE_ENTRY_DATA_KLAP = { - CONF_HOST: IP_ADDRESS, - CONF_ALIAS: ALIAS, - CONF_MODEL: MODEL, - CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_KLAP, - CONF_CONNECTION_PARAMETERS: CONN_PARAMS_KLAP.to_dict(), - CONF_USES_HTTP: True, -} -CREATE_ENTRY_DATA_AES = { - CONF_HOST: IP_ADDRESS2, - CONF_ALIAS: ALIAS, - CONF_MODEL: MODEL, - CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AES, - CONF_CONNECTION_PARAMETERS: CONN_PARAMS_AES.to_dict(), - CONF_USES_HTTP: True, - CONF_AES_KEYS: AES_KEYS, -} - def _load_feature_fixtures(): fixtures = load_json_value_fixture("features.json", DOMAIN) @@ -134,6 +60,7 @@ def _load_feature_fixtures(): FEATURES_FIXTURE = _load_feature_fixtures() +FIXTURE_ENUM_TYPES = {"CleanErrorCode": ErrorCode, "CleanAreaUnit": AreaUnit} async def setup_platform_for_device( @@ -147,7 +74,7 @@ async def setup_platform_for_device( _patch_discovery(device=device), _patch_connect(device=device), ): - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.config_entries.async_setup(config_entry.entry_id) # Good practice to wait background tasks in tests see PR #112726 await hass.async_block_till_done(wait_background_tasks=True) @@ -163,15 +90,15 @@ async def snapshot_platform( device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id) assert device_entries for device_entry in device_entries: - assert device_entry == snapshot( - name=f"{device_entry.name}-entry" - ), f"device entry snapshot failed for {device_entry.name}" + assert device_entry == snapshot(name=f"{device_entry.name}-entry"), ( + f"device entry snapshot failed for {device_entry.name}" + ) entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id) assert entity_entries - assert ( - len({entity_entry.domain for entity_entry in entity_entries}) == 1 - ), "Please limit the loaded platforms to 1 platform." + assert len({entity_entry.domain for entity_entry in entity_entries}) == 1, ( + "Please limit the loaded platforms to 1 platform." + ) translations = await async_get_translations(hass, "en", "entity", [DOMAIN]) unique_device_classes = [] @@ -179,22 +106,22 @@ async def snapshot_platform( if entity_entry.translation_key: key = f"component.{DOMAIN}.entity.{entity_entry.domain}.{entity_entry.translation_key}.name" single_device_class_translation = False - if key not in translations and entity_entry.original_device_class: + if key not in translations: # No name translation if entity_entry.original_device_class not in unique_device_classes: single_device_class_translation = True unique_device_classes.append(entity_entry.original_device_class) - assert ( - (key in translations) or single_device_class_translation - ), f"No translation or non unique device_class for entity {entity_entry.unique_id}, expected {key}" - assert entity_entry == snapshot( - name=f"{entity_entry.entity_id}-entry" - ), f"entity entry snapshot failed for {entity_entry.entity_id}" + assert (key in translations) or single_device_class_translation, ( + f"No translation or non unique device_class for entity {entity_entry.unique_id}, expected {key}" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry"), ( + f"entity entry snapshot failed for {entity_entry.entity_id}" + ) if entity_entry.disabled_by is None: state = hass.states.get(entity_entry.entity_id) assert state, f"State not found for {entity_entry.entity_id}" - assert state == snapshot( - name=f"{entity_entry.entity_id}-state" - ), f"state snapshot failed for {entity_entry.entity_id}" + assert state == snapshot(name=f"{entity_entry.entity_id}-state"), ( + f"state snapshot failed for {entity_entry.entity_id}" + ) async def setup_automation(hass: HomeAssistant, alias: str, entity_id: str) -> None: @@ -245,36 +172,66 @@ def _mocked_device( device.modules = {} device.features = {} + # replace device_config to prevent changes affecting between tests + device_config = replace(device_config) + if not ip_address: ip_address = IP_ADDRESS else: device_config.host = ip_address device.host = ip_address - if modules: - device.modules = { - module_name: MODULE_TO_MOCK_GEN[module_name](device) - for module_name in modules - } - + device_features = {} if features: - device.features = { + device_features = { feature_id: _mocked_feature(feature_id, require_fixture=True) for feature_id in features if isinstance(feature_id, str) } - device.features.update( + device_features.update( { feature.id: feature for feature in features if isinstance(feature, Feature) } ) + device.features = device_features + + # Add modules after features so modules can add any required features + if modules: + device.modules = { + module_name: MODULE_TO_MOCK_GEN[module_name](device) + for module_name in modules + } + + # module features are accessed from a module via get_feature which is + # keyed on the module attribute name. Usually this is the same as the + # feature.id but not always so accept overrides. + module_features = { + mod_key if (mod_key := v.expected_module_key) else k: v + for k, v in device_features.items() + } + for mod in device.modules.values(): + # Some tests remove the feature from device_features to test missing + # features, so check the key is still present there. + mod.get_feature.side_effect = ( + lambda mod_id: mod_feat + if (mod_feat := module_features.get(mod_id)) + and mod_feat.id in device_features + else None + ) + mod.has_feature.side_effect = ( + lambda mod_id: (mod_feat := module_features.get(mod_id)) + and mod_feat.id in device_features + ) + + device.parent = None device.children = [] if children: for child in children: child.mac = mac + child.parent = device device.children = children device.device_type = device_type if device_type else DeviceType.Unknown if ( @@ -289,6 +246,7 @@ def _mocked_device( device.protocol = _mock_protocol() device.config = device_config device.credentials_hash = credentials_hash + return device @@ -303,8 +261,9 @@ def _mocked_feature( precision_hint=None, choices=None, unit=None, - minimum_value=0, - maximum_value=2**16, # Arbitrary max + minimum_value=None, + maximum_value=None, + expected_module_key=None, ) -> Feature: """Get a mocked feature. @@ -314,15 +273,30 @@ def _mocked_feature( feature.id = id feature.name = name or id.upper() feature.set_value = AsyncMock() - if not (fixture := FEATURES_FIXTURE.get(id)): - assert ( - require_fixture is False - ), f"No fixture defined for feature {id} and require_fixture is True" - assert ( - value is not UNDEFINED - ), f"Value must be provided if feature {id} not defined in features.json" + if fixture := FEATURES_FIXTURE.get(id): + # copy the fixture so tests do not interfere with each other + fixture = dict(fixture) + + if enum_type := fixture.get("enum_type"): + val = FIXTURE_ENUM_TYPES[enum_type](fixture["value"]) + fixture["value"] = val + if timedelta_type := fixture.get("timedelta_type"): + fixture["value"] = timedelta(**{timedelta_type: fixture["value"]}) + + if unit_enum_type := fixture.get("unit_enum_type"): + val = FIXTURE_ENUM_TYPES[unit_enum_type](fixture["unit"]) + fixture["unit"] = val + + else: + assert require_fixture is False, ( + f"No fixture defined for feature {id} and require_fixture is True" + ) + assert value is not UNDEFINED, ( + f"Value must be provided if feature {id} not defined in features.json" + ) fixture = {"value": value, "category": "Primary", "type": "Sensor"} - elif value is not UNDEFINED: + + if value is not UNDEFINED: fixture["value"] = value feature.value = fixture["value"] @@ -334,11 +308,24 @@ def _mocked_feature( feature.unit = unit or fixture.get("unit") # number - feature.minimum_value = minimum_value or fixture.get("minimum_value") - feature.maximum_value = maximum_value or fixture.get("maximum_value") + min_val = minimum_value or fixture.get("minimum_value") + feature.minimum_value = 0 if min_val is None else min_val + max_val = maximum_value or fixture.get("maximum_value") + feature.maximum_value = 2**16 if max_val is None else max_val # select feature.choices = choices or fixture.get("choices") + + # module features are accessed from a module via get_feature which is + # keyed on the module attribute name. Usually this is the same as the + # feature.id but not always. module_key indicates the key of the feature + # in the module. + feature.expected_module_key = ( + mod_key + if (mod_key := fixture.get("expected_module_key", expected_module_key)) + else None + ) + return feature @@ -350,13 +337,7 @@ def _mocked_light_module(device) -> Light: light.state = LightState( light_on=True, brightness=light.brightness, color_temp=light.color_temp ) - light.is_color = True - light.is_variable_color_temp = True - light.is_dimmable = True - light.is_brightness = True - light.has_effects = False light.hsv = (10, 30, 5) - light.valid_temperature_range = ColorTempRange(min=4000, max=9000) light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} async def _set_state(state, *_, **__): @@ -389,18 +370,17 @@ def _mocked_light_module(device) -> Light: def _mocked_light_effect_module(device) -> LightEffect: effect = MagicMock(spec=LightEffect, name="Mocked light effect") - effect.has_effects = True effect.has_custom_effects = True effect.effect = "Effect1" effect.effect_list = ["Off", "Effect1", "Effect2"] async def _set_effect(effect_name, *_, **__): - assert ( - effect_name in effect.effect_list - ), f"set_effect '{effect_name}' not in {effect.effect_list}" - assert device.modules[ - Module.Light - ], "Need a light module to test set_effect method" + assert effect_name in effect.effect_list, ( + f"set_effect '{effect_name}' not in {effect.effect_list}" + ) + assert device.modules[Module.Light], ( + "Need a light module to test set_effect method" + ) device.modules[Module.Light].state.light_on = True effect.effect = effect_name @@ -419,12 +399,86 @@ def _mocked_fan_module(effect) -> Fan: def _mocked_alarm_module(device): alarm = MagicMock(auto_spec=Alarm, name="Mocked alarm") alarm.active = False + alarm.alarm_sounds = "Foo", "Bar" alarm.play = AsyncMock() alarm.stop = AsyncMock() + device.features["alarm_volume"] = _mocked_feature( + "alarm_volume", + minimum_value=0, + maximum_value=3, + value=None, + ) + device.features["alarm_duration"] = _mocked_feature( + "alarm_duration", + minimum_value=0, + maximum_value=300, + value=None, + ) + return alarm +def _mocked_camera_module(device): + camera = MagicMock(auto_spec=Camera, name="Mocked camera") + camera.is_on = True + camera.set_state = AsyncMock() + camera.stream_rtsp_url.return_value = ( + f"rtsp://user:pass@{device.host}:{LOCAL_STREAMING_PORT}/stream1" + ) + + return camera + + +def _mocked_thermostat_module(device): + therm = MagicMock(auto_spec=Thermostat, name="Mocked thermostat") + therm.state = True + therm.temperature = 20.2 + therm.target_temperature = 22.2 + therm.mode = ThermostatState.Heating + therm.set_state = AsyncMock() + therm.set_target_temperature = AsyncMock() + + return therm + + +def _mocked_clean_module(device): + clean = MagicMock(auto_spec=Clean, name="Mocked clean") + + # methods + clean.start = AsyncMock() + clean.pause = AsyncMock() + clean.resume = AsyncMock() + clean.return_home = AsyncMock() + clean.set_fan_speed_preset = AsyncMock() + + # properties + clean.fan_speed_preset = "Max" + clean.error = ErrorCode.Ok + clean.battery = 100 + clean.status = Status.Charged + + # Need to manually create the fan speed preset feature, + # as we are going to read its choices through it + device.features["vacuum_fan_speed"] = _mocked_feature( + "vacuum_fan_speed", + type_=Feature.Type.Choice, + category=Feature.Category.Config, + choices=["Quiet", "Max"], + value="Max", + expected_module_key="fan_speed_preset", + ) + + return clean + + +def _mocked_speaker_module(device): + speaker = MagicMock(auto_spec=Speaker, name="Mocked speaker") + speaker.locate = AsyncMock() + + return speaker + + def _mocked_strip_children(features=None, alias=None) -> list[Device]: plug0 = _mocked_device( alias="Plug0" if alias is None else alias, @@ -492,6 +546,10 @@ MODULE_TO_MOCK_GEN = { Module.LightEffect: _mocked_light_effect_module, Module.Fan: _mocked_fan_module, Module.Alarm: _mocked_alarm_module, + Module.Camera: _mocked_camera_module, + Module.Thermostat: _mocked_thermostat_module, + Module.Clean: _mocked_clean_module, + Module.Speaker: _mocked_speaker_module, } diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index 25a4bd20270..19cd5aa9acf 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -1,30 +1,73 @@ """tplink conftest.""" from collections.abc import Generator +from contextlib import contextmanager from unittest.mock import DEFAULT, AsyncMock, patch -from kasa import DeviceConfig +from kasa import DeviceConfig, Module import pytest from homeassistant.components.tplink import DOMAIN from homeassistant.core import HomeAssistant -from . import ( +from . import _mocked_device +from .const import ( + ALIAS_CAMERA, + CREATE_ENTRY_DATA_AES_CAMERA, CREATE_ENTRY_DATA_LEGACY, CREDENTIALS_HASH_AES, CREDENTIALS_HASH_KLAP, DEVICE_CONFIG_AES, + DEVICE_CONFIG_AES_CAMERA, DEVICE_CONFIG_KLAP, IP_ADDRESS, IP_ADDRESS2, + IP_ADDRESS3, MAC_ADDRESS, MAC_ADDRESS2, - _mocked_device, + MAC_ADDRESS3, + MODEL_CAMERA, ) from tests.common import MockConfigEntry +@contextmanager +def override_side_effect(mock: AsyncMock, effect): + """Temporarily override a mock side effect and replace afterwards.""" + try: + default_side_effect = mock.side_effect + mock.side_effect = effect + yield mock + finally: + mock.side_effect = default_side_effect + + +def _get_mock_devices(): + return { + IP_ADDRESS: _mocked_device( + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), + credentials_hash=CREDENTIALS_HASH_KLAP, + ip_address=IP_ADDRESS, + ), + IP_ADDRESS2: _mocked_device( + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_AES.to_dict()), + credentials_hash=CREDENTIALS_HASH_AES, + mac=MAC_ADDRESS2, + ip_address=IP_ADDRESS2, + ), + IP_ADDRESS3: _mocked_device( + device_config=DeviceConfig.from_dict(DEVICE_CONFIG_AES_CAMERA.to_dict()), + credentials_hash=CREDENTIALS_HASH_AES, + mac=MAC_ADDRESS3, + ip_address=IP_ADDRESS3, + modules=[Module.Camera], + alias=ALIAS_CAMERA, + model=MODEL_CAMERA, + ), + } + + @pytest.fixture def mock_discovery(): """Mock python-kasa discovery.""" @@ -34,22 +77,15 @@ def mock_discovery(): discover_single=DEFAULT, try_connect_all=DEFAULT, ) as mock_discovery: - device = _mocked_device( - device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), - credentials_hash=CREDENTIALS_HASH_KLAP, - alias="My Bulb", - ) - devices = { - "127.0.0.1": _mocked_device( - device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), - credentials_hash=CREDENTIALS_HASH_KLAP, - alias=None, - ) - } + devices = _get_mock_devices() + + def get_device(host, **kwargs): + return devices[host] + mock_discovery["discover"].return_value = devices - mock_discovery["discover_single"].return_value = device - mock_discovery["try_connect_all"].return_value = device - mock_discovery["mock_device"] = device + mock_discovery["discover_single"].side_effect = get_device + mock_discovery["try_connect_all"].side_effect = get_device + mock_discovery["mock_devices"] = devices yield mock_discovery @@ -57,22 +93,9 @@ def mock_discovery(): def mock_connect(): """Mock python-kasa connect.""" with patch("homeassistant.components.tplink.Device.connect") as mock_connect: - devices = { - IP_ADDRESS: _mocked_device( - device_config=DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()), - credentials_hash=CREDENTIALS_HASH_KLAP, - ip_address=IP_ADDRESS, - ), - IP_ADDRESS2: _mocked_device( - device_config=DeviceConfig.from_dict(DEVICE_CONFIG_AES.to_dict()), - credentials_hash=CREDENTIALS_HASH_AES, - mac=MAC_ADDRESS2, - ip_address=IP_ADDRESS2, - ), - } + devices = _get_mock_devices() def get_device(config): - nonlocal devices return devices[config.host] mock_connect.side_effect = get_device @@ -92,8 +115,12 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture -def mock_init() -> Generator[AsyncMock]: - """Override async_setup_entry.""" +def mock_init() -> Generator[dict[str, AsyncMock]]: + """Override async_setup and async_setup_entry. + + This fixture must be declared before the hass fixture to avoid errors + in the logs during teardown of the hass fixture which calls async_unload. + """ with patch.multiple( "homeassistant.components.tplink", async_setup=DEFAULT, @@ -117,6 +144,17 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +def mock_camera_config_entry() -> MockConfigEntry: + """Mock camera ConfigEntry.""" + return MockConfigEntry( + title="TPLink", + domain=DOMAIN, + data={**CREATE_ENTRY_DATA_AES_CAMERA}, + unique_id=MAC_ADDRESS3, + ) + + @pytest.fixture async def mock_added_config_entry( hass: HomeAssistant, diff --git a/tests/components/tplink/const.py b/tests/components/tplink/const.py new file mode 100644 index 00000000000..54aab1e2f3c --- /dev/null +++ b/tests/components/tplink/const.py @@ -0,0 +1,143 @@ +"""Constants for the tplink component tests.""" + +from kasa import ( + DeviceConfig, + DeviceConnectionParameters, + DeviceEncryptionType, + DeviceFamily, +) + +from homeassistant.components.tplink import ( + CONF_AES_KEYS, + CONF_ALIAS, + CONF_CAMERA_CREDENTIALS, + CONF_CONNECTION_PARAMETERS, + CONF_CREDENTIALS_HASH, + CONF_HOST, + CONF_LIVE_VIEW, + CONF_MODEL, + CONF_USES_HTTP, + Credentials, +) + +MODULE = "homeassistant.components.tplink" +MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" +IP_ADDRESS = "127.0.0.1" +IP_ADDRESS2 = "127.0.0.2" +IP_ADDRESS3 = "127.0.0.3" +ALIAS = "My Bulb" +ALIAS_CAMERA = "My Camera" +MODEL = "HS100" +MODEL_CAMERA = "C210" +MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +DEVICE_ID = "123456789ABCDEFGH" +DEVICE_ID_MAC = "AA:BB:CC:DD:EE:FF" +DHCP_FORMATTED_MAC_ADDRESS = MAC_ADDRESS.replace(":", "") +MAC_ADDRESS2 = "11:22:33:44:55:66" +MAC_ADDRESS3 = "66:55:44:33:22:11" +DEFAULT_ENTRY_TITLE = f"{ALIAS} {MODEL}" +DEFAULT_ENTRY_TITLE_CAMERA = f"{ALIAS_CAMERA} {MODEL_CAMERA}" +CREDENTIALS_HASH_LEGACY = "" +CONN_PARAMS_LEGACY = DeviceConnectionParameters( + DeviceFamily.IotSmartPlugSwitch, DeviceEncryptionType.Xor +) +DEVICE_CONFIG_LEGACY = DeviceConfig(IP_ADDRESS) +DEVICE_CONFIG_DICT_LEGACY = { + k: v for k, v in DEVICE_CONFIG_LEGACY.to_dict().items() if k != "credentials" +} +CREDENTIALS = Credentials("foo", "bar") +CREDENTIALS_HASH_AES = "AES/abcdefghijklmnopqrstuvabcdefghijklmnopqrstuv==" +CREDENTIALS_HASH_KLAP = "KLAP/abcdefghijklmnopqrstuv==" +CONN_PARAMS_KLAP = DeviceConnectionParameters( + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Klap +) +DEVICE_CONFIG_KLAP = DeviceConfig( + IP_ADDRESS, + credentials=CREDENTIALS, + connection_type=CONN_PARAMS_KLAP, +) +CONN_PARAMS_AES = DeviceConnectionParameters( + DeviceFamily.SmartTapoPlug, DeviceEncryptionType.Aes +) +_test_privkey = ( + "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKLJKmBWGj6WYo9sewI8vkqar" + "Ed5H1JUr8Jj/LEWLTtV6+Mm4mfyEk6YKFHSmIG4AGgrVsGK/EbEkTZk9CwtixNQpBVc36oN2R" + "vuWWV38YnP4vI63mNxTA/gQonCsahjN4HfwE87pM7O5z39aeunoYm6Be663t33DbJH1ZUbZjm" + "tAgMBAAECgYB1Bn1KaFvRprcQOIJt51E9vNghQbf8rhj0fIEKpdC6mVhNIoUdCO+URNqnh+hP" + "SQIx4QYreUlHbsSeABFxOQSDJm6/kqyQsp59nCVDo/bXTtlvcSJ/sU3riqJNxYqEU1iJ0xMvU" + "N1VKKTmik89J8e5sN9R0AFfUSJIk7MpdOoD2QJBANTbV27nenyvbqee/ul4frdt2rrPGcGpcV" + "QmY87qbbrZgqgL5LMHHD7T/v/I8D1wRog1sBz/AiZGcnv/ox8dHKsCQQDDx8DCGPySSVqKVua" + "yUkBNpglN83wiCXZjyEtWIt+aB1A2n5ektE/o8oHnnOuvMdooxvtid7Mdapi2VLHV7VMHAkAE" + "d0GjWwnv2cJpk+VnQpbuBEkFiFjS/loZWODZM4Pv2qZqHi3DL9AA5XPBLBcWQufH7dBvG06RP" + "QMj5N4oRfUXAkEAuJJkVliqHNvM4OkGewzyFII4+WVYHNqg43dcFuuvtA27AJQ6qYtYXrvp3k" + "phI3yzOIhHTNCea1goepSkR5ODFwJBAJCTRbB+P47aEr/xA51ZFHE6VefDBJG9yg6yK4jcOxg" + "5ficXEpx8442okNtlzwa+QHpm/L3JOFrHwiEeVqXtiqY=" +) +_test_pubkey = ( + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiySpgVho+lmKPbHsCPL5KmqxHeR9SVK/CY" + "/yxFi07VevjJuJn8hJOmChR0piBuABoK1bBivxGxJE2ZPQsLYsTUKQVXN+qDdkb7llld/GJz+" + "LyOt5jcUwP4EKJwrGoYzeB38BPO6TOzuc9/Wnrp6GJugXuut7d9w2yR9WVG2Y5rQIDAQAB" +) +AES_KEYS = {"private": _test_privkey, "public": _test_pubkey} +DEVICE_CONFIG_AES = DeviceConfig( + IP_ADDRESS2, + credentials=CREDENTIALS, + connection_type=CONN_PARAMS_AES, + aes_keys=AES_KEYS, +) +CONN_PARAMS_AES_CAMERA = DeviceConnectionParameters( + DeviceFamily.SmartIpCamera, DeviceEncryptionType.Aes, https=True, login_version=2 +) +DEVICE_CONFIG_AES_CAMERA = DeviceConfig( + IP_ADDRESS3, + credentials=CREDENTIALS, + connection_type=CONN_PARAMS_AES_CAMERA, +) + +DEVICE_CONFIG_DICT_KLAP = { + k: v for k, v in DEVICE_CONFIG_KLAP.to_dict().items() if k != "credentials" +} +DEVICE_CONFIG_DICT_AES = { + k: v for k, v in DEVICE_CONFIG_AES.to_dict().items() if k != "credentials" +} +CREATE_ENTRY_DATA_LEGACY = { + CONF_HOST: IP_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_MODEL: MODEL, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_LEGACY.to_dict(), + CONF_USES_HTTP: False, +} + +CREATE_ENTRY_DATA_KLAP = { + CONF_HOST: IP_ADDRESS, + CONF_ALIAS: ALIAS, + CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_KLAP, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_KLAP.to_dict(), + CONF_USES_HTTP: True, +} +CREATE_ENTRY_DATA_AES = { + CONF_HOST: IP_ADDRESS2, + CONF_ALIAS: ALIAS, + CONF_MODEL: MODEL, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AES, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_AES.to_dict(), + CONF_USES_HTTP: True, + CONF_AES_KEYS: AES_KEYS, +} +CREATE_ENTRY_DATA_AES_CAMERA = { + CONF_HOST: IP_ADDRESS3, + CONF_ALIAS: ALIAS_CAMERA, + CONF_MODEL: MODEL_CAMERA, + CONF_CREDENTIALS_HASH: CREDENTIALS_HASH_AES, + CONF_CONNECTION_PARAMETERS: CONN_PARAMS_AES_CAMERA.to_dict(), + CONF_USES_HTTP: True, + CONF_LIVE_VIEW: True, + CONF_CAMERA_CREDENTIALS: {"username": "camuser", "password": "campass"}, +} +SMALLEST_VALID_JPEG = ( + "ffd8ffe000104a46494600010101004800480000ffdb00430003020202020203020202030303030406040404040408060" + "6050609080a0a090809090a0c0f0c0a0b0e0b09090d110d0e0f101011100a0c12131210130f101010ffc9000b08000100" + "0101011100ffcc000600101005ffda0008010100003f00d2cf20ffd9" +) +SMALLEST_VALID_JPEG_BYTES = bytes.fromhex(SMALLEST_VALID_JPEG) diff --git a/tests/components/tplink/fixtures/features.json b/tests/components/tplink/fixtures/features.json index f60132fd2c2..81277ddd3ae 100644 --- a/tests/components/tplink/fixtures/features.json +++ b/tests/components/tplink/fixtures/features.json @@ -44,6 +44,26 @@ "type": "Switch", "category": "Config" }, + "motion_detection": { + "value": true, + "type": "Switch", + "category": "Primary" + }, + "person_detection": { + "value": true, + "type": "Switch", + "category": "Primary" + }, + "tamper_detection": { + "value": true, + "type": "Switch", + "category": "Primary" + }, + "baby_cry_detection": { + "value": true, + "type": "Switch", + "category": "Primary" + }, "current_consumption": { "value": 5.23, "type": "Sensor", @@ -175,6 +195,11 @@ "type": "BinarySensor", "category": "Info" }, + "overloaded": { + "value": false, + "type": "BinarySensor", + "category": "Info" + }, "battery_low": { "value": false, "type": "BinarySensor", @@ -264,10 +289,18 @@ "minimum_value": -10, "maximum_value": 10 }, + "power_protection_threshold": { + "value": 100, + "type": "Number", + "category": "Config", + "minimum_value": 0 + }, "target_temperature": { "value": false, "type": "Number", - "category": "Primary" + "category": "Primary", + "minimum_value": 5, + "maximum_value": 30 }, "fan_speed_level": { "value": 2, @@ -308,6 +341,61 @@ "Connection 2" ] }, + "clean_time": { + "type": "Sensor", + "category": "Info", + "value": 12, + "timedelta_type": "minutes" + }, + "clean_area": { + "type": "Sensor", + "category": "Info", + "value": 2, + "unit": 1, + "unit_enum_type": "CleanAreaUnit" + }, + "clean_progress": { + "type": "Sensor", + "category": "Info", + "value": 30, + "unit": "%" + }, + "total_clean_time": { + "type": "Sensor", + "category": "Debug", + "value": 120, + "timedelta_type": "minutes" + }, + "total_clean_area": { + "type": "Sensor", + "category": "Debug", + "value": 2, + "unit": 1, + "unit_enum_type": "CleanAreaUnit" + }, + "last_clean_time": { + "type": "Sensor", + "category": "Debug", + "value": 60, + "timedelta_type": "minutes" + }, + "last_clean_area": { + "type": "Sensor", + "category": "Debug", + "value": 2, + "unit": 1, + "unit_enum_type": "CleanAreaUnit" + }, + "last_clean_timestamp": { + "type": "Sensor", + "category": "Debug", + "value": "2024-06-24 10:03:11.046643+01:00" + }, + "total_clean_count": { + "type": "Sensor", + "category": "Debug", + "value": 12 + }, "alarm_volume": { "value": "normal", "type": "Choice", @@ -318,5 +406,146 @@ "type": "Sensor", "category": "Info", "value": "2024-06-24 10:03:11.046643+01:00" + }, + "pan_left": { + "value": "", + "type": "Action", + "category": "Config" + }, + "pan_right": { + "value": "", + "type": "Action", + "category": "Config" + }, + "pan_step": { + "value": 10, + "type": "Number", + "category": "Config" + }, + "tilt_up": { + "value": "", + "type": "Action", + "category": "Config" + }, + "tilt_down": { + "value": "", + "type": "Action", + "category": "Config" + }, + "tilt_step": { + "value": 10, + "type": "Number", + "category": "Config" + }, + "clean_count": { + "value": 1, + "type": "Number", + "category": "Config" + }, + "carpet_boost": { + "value": true, + "type": "Switch", + "category": "Config" + }, + "vacuum_error": { + "value": 0, + "type": "Sensor", + "category": "Info", + "enum_type": "CleanErrorCode" + }, + "pair": { + "value": "", + "type": "Action", + "category": "Config" + }, + "unpair": { + "value": "", + "type": "Action", + "category": "Debug" + }, + "main_brush_reset": { + "value": "", + "type": "Action", + "category": "Debug" + }, + "side_brush_reset": { + "value": "", + "type": "Action", + "category": "Debug" + }, + "sensor_reset": { + "value": "", + "type": "Action", + "category": "Debug" + }, + "filter_reset": { + "value": "", + "type": "Action", + "category": "Debug" + }, + "charging_contacts_reset": { + "value": "", + "type": "Action", + "category": "Debug" + }, + "main_brush_remaining": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "main_brush_used": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "side_brush_remaining": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "side_brush_used": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "filter_remaining": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "filter_used": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "sensor_remaining": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "sensor_used": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "charging_contacts_remaining": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" + }, + "charging_contacts_used": { + "value": 360, + "type": "Sensor", + "category": "Debug", + "timedelta_type": "minutes" } } diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index 4a1cfe5b411..17aa2c248e5 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -1,17 +1,18 @@ # serializer version: 1 -# name: test_states[binary_sensor.my_device_battery_low-entry] +# name: test_states[binary_sensor.my_device_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.my_device_battery_low', + 'entity_id': 'binary_sensor.my_device_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -23,7 +24,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery low', + 'original_name': 'Battery', 'platform': 'tplink', 'previous_unique_id': None, 'supported_features': 0, @@ -39,6 +40,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -86,6 +88,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -133,6 +136,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -166,6 +170,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -213,6 +218,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -260,6 +266,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -300,6 +307,54 @@ 'state': 'off', }) # --- +# name: test_states[binary_sensor.my_device_overloaded-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.my_device_overloaded', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Overloaded', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'overloaded', + 'unique_id': '123456789ABCDEFGH_overloaded', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[binary_sensor.my_device_overloaded-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'my_device Overloaded', + }), + 'context': , + 'entity_id': 'binary_sensor.my_device_overloaded', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_states[binary_sensor.my_device_temperature_warning-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -307,6 +362,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -337,6 +393,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index bb75f4642e1..bb4e9f85d58 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -1,4 +1,315 @@ # serializer version: 1 +# name: test_states[button.my_device_pair_new_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_pair_new_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pair new device', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pair', + 'unique_id': '123456789ABCDEFGH_pair', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_pair_new_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Pair new device', + }), + 'context': , + 'entity_id': 'button.my_device_pair_new_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_pan_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_pan_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pan left', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pan_left', + 'unique_id': '123456789ABCDEFGH_pan_left', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_pan_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Pan left', + }), + 'context': , + 'entity_id': 'button.my_device_pan_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_pan_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_pan_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pan right', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pan_right', + 'unique_id': '123456789ABCDEFGH_pan_right', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_pan_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Pan right', + }), + 'context': , + 'entity_id': 'button.my_device_pan_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_reset_charging_contacts_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_reset_charging_contacts_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset charging contacts consumable', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_contacts_reset', + 'unique_id': '123456789ABCDEFGH_charging_contacts_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_reset_filter_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_reset_filter_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset filter consumable', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'filter_reset', + 'unique_id': '123456789ABCDEFGH_filter_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_reset_main_brush_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_reset_main_brush_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset main brush consumable', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'main_brush_reset', + 'unique_id': '123456789ABCDEFGH_main_brush_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_reset_sensor_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_reset_sensor_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset sensor consumable', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensor_reset', + 'unique_id': '123456789ABCDEFGH_sensor_reset', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_reset_side_brush_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_reset_side_brush_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset side brush consumable', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'side_brush_reset', + 'unique_id': '123456789ABCDEFGH_side_brush_reset', + 'unit_of_measurement': None, + }) +# --- # name: test_states[button.my_device_restart-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6,6 +317,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -39,6 +351,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -85,6 +398,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -124,10 +438,139 @@ 'state': 'unknown', }) # --- +# name: test_states[button.my_device_tilt_down-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_tilt_down', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tilt down', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tilt_down', + 'unique_id': '123456789ABCDEFGH_tilt_down', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_tilt_down-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Tilt down', + }), + 'context': , + 'entity_id': 'button.my_device_tilt_down', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_tilt_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_tilt_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tilt up', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tilt_up', + 'unique_id': '123456789ABCDEFGH_tilt_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[button.my_device_tilt_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Tilt up', + }), + 'context': , + 'entity_id': 'button.my_device_tilt_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_states[button.my_device_unpair_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.my_device_unpair_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Unpair device', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'unpair', + 'unique_id': '123456789ABCDEFGH_unpair', + 'unit_of_measurement': None, + }) +# --- # name: test_states[my_device-entry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr new file mode 100644 index 00000000000..e037c2c9e40 --- /dev/null +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -0,0 +1,89 @@ +# serializer version: 1 +# name: test_states[camera.my_camera_live_view-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'camera', + 'entity_category': None, + 'entity_id': 'camera.my_camera_live_view', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Live view', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'live_view', + 'unique_id': '123456789ABCDEFGH-live_view', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[camera.my_camera_live_view-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1caab5c3b3', + 'entity_picture': '/api/camera_proxy/camera.my_camera_live_view?token=1caab5c3b3', + 'friendly_name': 'my_camera Live view', + 'frontend_stream_type': , + 'supported_features': , + }), + 'context': , + 'entity_id': 'camera.my_camera_live_view', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_states[my_camera-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '66:55:44:33:22:11', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'model_id': None, + 'name': 'my_camera', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index 8236f332046..02492de92b9 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -9,10 +9,11 @@ , , ]), - 'max_temp': 65536, - 'min_temp': None, + 'max_temp': 30, + 'min_temp': 5, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -49,8 +50,8 @@ , , ]), - 'max_temp': 65536, - 'min_temp': None, + 'max_temp': 30, + 'min_temp': 5, 'supported_features': , 'temperature': 22.2, }), @@ -66,6 +67,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -91,6 +93,6 @@ 'serial_number': None, 'suggested_area': None, 'sw_version': '1.0.0', - 'via_device_id': None, + 'via_device_id': , }) # --- diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index 1a7392dc63a..9c395dc2f21 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -8,6 +8,7 @@ 'preset_modes': None, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -61,6 +62,7 @@ 'preset_modes': None, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -114,6 +116,7 @@ 'preset_modes': None, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -162,6 +165,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 977d2098fb9..0415039a0ce 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -35,7 +36,63 @@ 'via_device_id': None, }) # --- -# name: test_states[number.my_device_smooth_off-entry] +# name: test_states[number.my_device_clean_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_clean_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Clean count', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clean_count', + 'unique_id': '123456789ABCDEFGH_clean_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_clean_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Clean count', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_clean_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_states[number.my_device_pan_degrees-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -47,6 +104,119 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_pan_degrees', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pan degrees', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pan_step', + 'unique_id': '123456789ABCDEFGH_pan_step', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_pan_degrees-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Pan degrees', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_pan_degrees', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_states[number.my_device_power_protection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_power_protection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power protection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_protection_threshold', + 'unique_id': '123456789ABCDEFGH_power_protection_threshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_power_protection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Power protection', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_power_protection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_states[number.my_device_smooth_off-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 60, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -77,7 +247,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'my_device Smooth off', - 'max': 65536, + 'max': 60, 'min': 0, 'mode': , 'step': 1.0, @@ -96,12 +266,13 @@ }), 'area_id': None, 'capabilities': dict({ - 'max': 65536, + 'max': 60, 'min': 0, 'mode': , 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -132,7 +303,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'my_device Smooth on', - 'max': 65536, + 'max': 60, 'min': 0, 'mode': , 'step': 1.0, @@ -151,12 +322,13 @@ }), 'area_id': None, 'capabilities': dict({ - 'max': 65536, + 'max': 10, 'min': -10, 'mode': , 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -187,7 +359,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'my_device Temperature offset', - 'max': 65536, + 'max': 10, 'min': -10, 'mode': , 'step': 1.0, @@ -200,7 +372,7 @@ 'state': 'False', }) # --- -# name: test_states[number.my_device_turn_off_in-entry] +# name: test_states[number.my_device_tilt_degrees-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -212,6 +384,63 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.my_device_tilt_degrees', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tilt degrees', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tilt_step', + 'unique_id': '123456789ABCDEFGH_tilt_step', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[number.my_device_tilt_degrees-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Tilt degrees', + 'max': 65536, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.my_device_tilt_degrees', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_states[number.my_device_turn_off_in-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 60, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -242,7 +471,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'my_device Turn off in', - 'max': 65536, + 'max': 60, 'min': 0, 'mode': , 'step': 1.0, diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index c851979f34c..e5191937ee9 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -64,6 +65,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -137,6 +139,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +197,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 739f02e51f0..72198e579a1 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -42,6 +43,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -75,6 +77,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -115,7 +118,7 @@ 'state': '2024-06-24T09:03:11+00:00', }) # --- -# name: test_states[sensor.my_device_battery_level-entry] +# name: test_states[sensor.my_device_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -124,12 +127,13 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.my_device_battery_level', + 'entity_id': 'sensor.my_device_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -141,7 +145,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Battery level', + 'original_name': 'Battery', 'platform': 'tplink', 'previous_unique_id': None, 'supported_features': 0, @@ -150,22 +154,239 @@ 'unit_of_measurement': '%', }) # --- -# name: test_states[sensor.my_device_battery_level-state] +# name: test_states[sensor.my_device_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'my_device Battery level', + 'friendly_name': 'my_device Battery', 'state_class': , 'unit_of_measurement': '%', }), 'context': , - 'entity_id': 'sensor.my_device_battery_level', + 'entity_id': 'sensor.my_device_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '85', }) # --- +# name: test_states[sensor.my_device_charging_contacts_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_charging_contacts_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging contacts remaining', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_contacts_remaining', + 'unique_id': '123456789ABCDEFGH_charging_contacts_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_charging_contacts_used-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_charging_contacts_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging contacts used', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_contacts_used', + 'unique_id': '123456789ABCDEFGH_charging_contacts_used', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_cleaning_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_cleaning_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cleaning area', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clean_area', + 'unique_id': '123456789ABCDEFGH_clean_area', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_cleaning_area-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'area', + 'friendly_name': 'my_device Cleaning area', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_device_cleaning_area', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.2', + }) +# --- +# name: test_states[sensor.my_device_cleaning_progress-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_cleaning_progress', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cleaning progress', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clean_progress', + 'unique_id': '123456789ABCDEFGH_clean_progress', + 'unit_of_measurement': '%', + }) +# --- +# name: test_states[sensor.my_device_cleaning_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_cleaning_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cleaning time', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'clean_time', + 'unique_id': '123456789ABCDEFGH_clean_time', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_cleaning_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'my_device Cleaning time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.my_device_cleaning_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.00', + }) +# --- # name: test_states[sensor.my_device_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -175,6 +396,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -229,6 +451,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -281,6 +504,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -307,6 +531,157 @@ 'unit_of_measurement': None, }) # --- +# name: test_states[sensor.my_device_error-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'sidebrushstuck', + 'mainbrushstuck', + 'wheelblocked', + 'trapped', + 'trappedcliff', + 'dustbinremoved', + 'unabletomove', + 'lidarblocked', + 'unabletofinddock', + 'batterylow', + 'unknowninternal', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_error', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Error', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vacuum_error', + 'unique_id': '123456789ABCDEFGH_vacuum_error', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_error-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'my_device Error', + 'options': list([ + 'ok', + 'sidebrushstuck', + 'mainbrushstuck', + 'wheelblocked', + 'trapped', + 'trappedcliff', + 'dustbinremoved', + 'unabletomove', + 'lidarblocked', + 'unabletofinddock', + 'batterylow', + 'unknowninternal', + ]), + }), + 'context': , + 'entity_id': 'sensor.my_device_error', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_states[sensor.my_device_filter_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_filter_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter remaining', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'filter_remaining', + 'unique_id': '123456789ABCDEFGH_filter_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_filter_used-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_filter_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter used', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'filter_used', + 'unique_id': '123456789ABCDEFGH_filter_used', + 'unit_of_measurement': , + }) +# --- # name: test_states[sensor.my_device_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -316,6 +691,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -358,6 +734,116 @@ 'state': '12', }) # --- +# name: test_states[sensor.my_device_last_clean_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_last_clean_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last clean start', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_clean_timestamp', + 'unique_id': '123456789ABCDEFGH_last_clean_timestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_last_cleaned_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_last_cleaned_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last cleaned area', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_clean_area', + 'unique_id': '123456789ABCDEFGH_last_clean_area', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_last_cleaned_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_last_cleaned_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last cleaned time', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_clean_time', + 'unique_id': '123456789ABCDEFGH_last_clean_time', + 'unit_of_measurement': , + }) +# --- # name: test_states[sensor.my_device_last_water_leak_alert-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -365,6 +851,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -405,6 +892,80 @@ 'state': '2024-06-24T09:03:11+00:00', }) # --- +# name: test_states[sensor.my_device_main_brush_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_main_brush_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Main brush remaining', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'main_brush_remaining', + 'unique_id': '123456789ABCDEFGH_main_brush_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_main_brush_used-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_main_brush_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Main brush used', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'main_brush_used', + 'unique_id': '123456789ABCDEFGH_main_brush_used', + 'unit_of_measurement': , + }) +# --- # name: test_states[sensor.my_device_on_since-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -412,6 +973,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -445,6 +1007,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -471,6 +1034,154 @@ 'unit_of_measurement': '%', }) # --- +# name: test_states[sensor.my_device_sensor_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_sensor_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sensor remaining', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensor_remaining', + 'unique_id': '123456789ABCDEFGH_sensor_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_sensor_used-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_sensor_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sensor used', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensor_used', + 'unique_id': '123456789ABCDEFGH_sensor_used', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_side_brush_remaining-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_side_brush_remaining', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Side brush remaining', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'side_brush_remaining', + 'unique_id': '123456789ABCDEFGH_side_brush_remaining', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_side_brush_used-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_side_brush_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Side brush used', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'side_brush_used', + 'unique_id': '123456789ABCDEFGH_side_brush_used', + 'unit_of_measurement': , + }) +# --- # name: test_states[sensor.my_device_signal_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -480,6 +1191,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -529,6 +1241,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -562,6 +1275,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -597,6 +1311,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': , @@ -632,6 +1347,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -686,6 +1402,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -731,6 +1448,120 @@ 'state': '5.23', }) # --- +# name: test_states[sensor.my_device_total_cleaning_area-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_total_cleaning_area', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning area', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_clean_area', + 'unique_id': '123456789ABCDEFGH_total_clean_area', + 'unit_of_measurement': , + }) +# --- +# name: test_states[sensor.my_device_total_cleaning_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_total_cleaning_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Total cleaning count', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_clean_count', + 'unique_id': '123456789ABCDEFGH_total_clean_count', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[sensor.my_device_total_cleaning_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.my_device_total_cleaning_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total cleaning time', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_clean_time', + 'unique_id': '123456789ABCDEFGH_total_clean_time', + 'unit_of_measurement': , + }) +# --- # name: test_states[sensor.my_device_total_consumption-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -740,6 +1571,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -794,6 +1626,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tplink/snapshots/test_siren.ambr b/tests/components/tplink/snapshots/test_siren.ambr index b144288bd1c..7365e449707 100644 --- a/tests/components/tplink/snapshots/test_siren.ambr +++ b/tests/components/tplink/snapshots/test_siren.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -40,8 +41,14 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'available_tones': tuple( + 'Foo', + 'Bar', + ), + }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -62,7 +69,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH', 'unit_of_measurement': None, @@ -71,8 +78,12 @@ # name: test_states[siren.hub-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'available_tones': tuple( + 'Foo', + 'Bar', + ), 'friendly_name': 'hub', - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'siren.hub', diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index 36c630474c8..bd89da8e841 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ tuple( @@ -42,6 +43,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -88,6 +90,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -134,6 +137,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -173,6 +177,100 @@ 'state': 'on', }) # --- +# name: test_states[switch.my_device_baby_cry_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.my_device_baby_cry_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Baby cry detection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'baby_cry_detection', + 'unique_id': '123456789ABCDEFGH_baby_cry_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_baby_cry_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Baby cry detection', + }), + 'context': , + 'entity_id': 'switch.my_device_baby_cry_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_states[switch.my_device_carpet_boost-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.my_device_carpet_boost', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Carpet boost', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'carpet_boost', + 'unique_id': '123456789ABCDEFGH_carpet_boost', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_carpet_boost-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Carpet boost', + }), + 'context': , + 'entity_id': 'switch.my_device_carpet_boost', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_states[switch.my_device_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -180,6 +278,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -226,6 +325,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -272,6 +372,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -311,6 +412,53 @@ 'state': 'on', }) # --- +# name: test_states[switch.my_device_motion_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.my_device_motion_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motion detection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'motion_detection', + 'unique_id': '123456789ABCDEFGH_motion_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_motion_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Motion detection', + }), + 'context': , + 'entity_id': 'switch.my_device_motion_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_states[switch.my_device_motion_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -318,6 +466,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -357,6 +506,53 @@ 'state': 'on', }) # --- +# name: test_states[switch.my_device_person_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.my_device_person_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Person detection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'person_detection', + 'unique_id': '123456789ABCDEFGH_person_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_person_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Person detection', + }), + 'context': , + 'entity_id': 'switch.my_device_person_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_states[switch.my_device_smooth_transitions-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -364,6 +560,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -403,3 +600,50 @@ 'state': 'on', }) # --- +# name: test_states[switch.my_device_tamper_detection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.my_device_tamper_detection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tamper detection', + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tamper_detection', + 'unique_id': '123456789ABCDEFGH_tamper_detection', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[switch.my_device_tamper_detection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'my_device Tamper detection', + }), + 'context': , + 'entity_id': 'switch.my_device_tamper_detection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/tplink/snapshots/test_vacuum.ambr b/tests/components/tplink/snapshots/test_vacuum.ambr new file mode 100644 index 00000000000..e010c9545d1 --- /dev/null +++ b/tests/components/tplink/snapshots/test_vacuum.ambr @@ -0,0 +1,98 @@ +# serializer version: 1 +# name: test_states[my_vacuum-entry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '1.0.0', + 'id': , + 'identifiers': set({ + tuple( + 'tplink', + '123456789ABCDEFGH', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'TP-Link', + 'model': 'HS100', + 'model_id': None, + 'name': 'my_vacuum', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }) +# --- +# name: test_states[vacuum.my_vacuum-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_speed_list': list([ + 'quiet', + 'max', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'vacuum', + 'entity_category': None, + 'entity_id': 'vacuum.my_vacuum', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tplink', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'vacuum', + 'unique_id': '123456789ABCDEFGH-vacuum', + 'unit_of_measurement': None, + }) +# --- +# name: test_states[vacuum.my_vacuum-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'battery_icon': 'mdi:battery-charging-100', + 'battery_level': 100, + 'fan_speed': 'max', + 'fan_speed_list': list([ + 'quiet', + 'max', + ]), + 'friendly_name': 'my_vacuum', + 'supported_features': , + }), + 'context': , + 'entity_id': 'vacuum.my_vacuum', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'docked', + }) +# --- diff --git a/tests/components/tplink/test_binary_sensor.py b/tests/components/tplink/test_binary_sensor.py index e2b9cd08d13..b487fa51baf 100644 --- a/tests/components/tplink/test_binary_sensor.py +++ b/tests/components/tplink/test_binary_sensor.py @@ -4,18 +4,14 @@ from kasa import Feature import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import tplink from homeassistant.components.tplink.binary_sensor import BINARY_SENSOR_DESCRIPTIONS from homeassistant.components.tplink.const import DOMAIN from homeassistant.components.tplink.entity import EXCLUDED_FEATURES from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, _mocked_feature, _mocked_strip_children, @@ -24,6 +20,7 @@ from . import ( setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry @@ -47,7 +44,7 @@ async def test_states( device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test a sensor unique ids.""" + """Test binary sensor states.""" features = {description.key for description in BINARY_SENSOR_DESCRIPTIONS} features.update(EXCLUDED_FEATURES) device = _mocked_device(alias="my_device", features=features) @@ -68,7 +65,7 @@ async def test_binary_sensor( entity_registry: er.EntityRegistry, mocked_feature_binary_sensor: Feature, ) -> None: - """Test a sensor unique ids.""" + """Test binary sensor unique ids.""" mocked_feature = mocked_feature_binary_sensor already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS @@ -77,7 +74,7 @@ async def test_binary_sensor( plug = _mocked_device(alias="my_plug", features=[mocked_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() # The entity_id is based on standard name from core. @@ -93,7 +90,7 @@ async def test_binary_sensor_children( device_registry: dr.DeviceRegistry, mocked_feature_binary_sensor: Feature, ) -> None: - """Test a sensor unique ids.""" + """Test binary sensor children.""" mocked_feature = mocked_feature_binary_sensor already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS @@ -105,7 +102,7 @@ async def test_binary_sensor_children( children=_mocked_strip_children(features=[mocked_feature]), ) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "binary_sensor.my_plug_overheated" diff --git a/tests/components/tplink/test_button.py b/tests/components/tplink/test_button.py index a3eb8950336..c36d08337a7 100644 --- a/tests/components/tplink/test_button.py +++ b/tests/components/tplink/test_button.py @@ -4,7 +4,6 @@ from kasa import Feature import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import tplink from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.tplink.button import BUTTON_DESCRIPTIONS from homeassistant.components.tplink.const import DOMAIN @@ -16,11 +15,8 @@ from homeassistant.helpers import ( entity_registry as er, issue_registry as ir, ) -from homeassistant.setup import async_setup_component from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, _mocked_feature, _mocked_strip_children, @@ -30,6 +26,7 @@ from . import ( setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry @@ -83,7 +80,7 @@ def create_deprecated_child_button_entities( @pytest.fixture def mocked_feature_button() -> Feature: - """Return mocked tplink binary sensor feature.""" + """Return mocked tplink button feature.""" return _mocked_feature( "test_alarm", value="", @@ -101,7 +98,7 @@ async def test_states( snapshot: SnapshotAssertion, create_deprecated_button_entities, ) -> None: - """Test a sensor unique ids.""" + """Test button states.""" features = {description.key for description in BUTTON_DESCRIPTIONS} features.update(EXCLUDED_FEATURES) device = _mocked_device(alias="my_device", features=features) @@ -118,14 +115,15 @@ async def test_states( async def test_button( hass: HomeAssistant, entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, mocked_feature_button: Feature, create_deprecated_button_entities, ) -> None: - """Test a sensor unique ids.""" + """Test button unique ids.""" mocked_feature = mocked_feature_button plug = _mocked_device(alias="my_device", features=[mocked_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # The entity_id is based on standard name from core. @@ -139,11 +137,12 @@ async def test_button_children( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, mocked_feature_button: Feature, create_deprecated_button_entities, create_deprecated_child_button_entities, ) -> None: - """Test a sensor unique ids.""" + """Test button children.""" mocked_feature = mocked_feature_button plug = _mocked_device( alias="my_device", @@ -151,7 +150,7 @@ async def test_button_children( children=_mocked_strip_children(features=[mocked_feature]), ) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() entity_id = "button.my_device_test_alarm" @@ -173,6 +172,7 @@ async def test_button_children( async def test_button_press( hass: HomeAssistant, entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, mocked_feature_button: Feature, create_deprecated_button_entities, ) -> None: @@ -180,7 +180,7 @@ async def test_button_press( mocked_feature = mocked_feature_button plug = _mocked_device(alias="my_device", features=[mocked_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() entity_id = "button.my_device_test_alarm" @@ -213,7 +213,7 @@ async def test_button_not_exists_with_deprecation( mocked_feature = mocked_feature_button dev = _mocked_device(alias="my_device", features=[mocked_feature]) with _patch_discovery(device=dev), _patch_connect(device=dev): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert not entity_registry.async_get(entity_id) @@ -265,7 +265,7 @@ async def test_button_exists_with_deprecation( mocked_feature = mocked_feature_button dev = _mocked_device(alias="my_device", features=[mocked_feature]) with _patch_discovery(device=dev), _patch_connect(device=dev): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() entity = entity_registry.async_get(entity_id) diff --git a/tests/components/tplink/test_camera.py b/tests/components/tplink/test_camera.py new file mode 100644 index 00000000000..4b062c4d0b2 --- /dev/null +++ b/tests/components/tplink/test_camera.py @@ -0,0 +1,454 @@ +"""The tests for the tplink camera platform.""" + +import asyncio +from unittest.mock import AsyncMock, patch + +from aiohttp.test_utils import make_mocked_request +from freezegun.api import FrozenDateTimeFactory +from kasa import Module +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import stream +from homeassistant.components.camera import ( + DOMAIN as CAMERA_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + CameraEntityFeature, + StreamType, + async_get_image, + async_get_mjpeg_stream, + get_camera_from_entity_id, +) +from homeassistant.components.tplink.camera import TPLinkCameraEntity +from homeassistant.components.websocket_api import TYPE_RESULT +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import _mocked_device, setup_platform_for_device, snapshot_platform +from .const import DEVICE_ID, IP_ADDRESS3, MAC_ADDRESS3, SMALLEST_VALID_JPEG_BYTES + +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator + + +async def test_states( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test camera states.""" + mock_camera_config_entry.add_to_hass(hass) + + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + # Patch getrandbits so the access_token doesn't change on camera attributes + with patch("random.SystemRandom.getrandbits", return_value=123123123123): + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + await snapshot_platform( + hass, + entity_registry, + device_registry, + snapshot, + mock_camera_config_entry.entry_id, + ) + + +async def test_camera_unique_id( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test camera unique id.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + device_id=DEVICE_ID, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + device_entries = dr.async_entries_for_config_entry( + device_registry, mock_camera_config_entry.entry_id + ) + assert device_entries + entity_id = "camera.my_camera_live_view" + + assert entity_registry.async_get(entity_id).unique_id == f"{DEVICE_ID}-live_view" + + +async def test_handle_mjpeg_stream( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, +) -> None: + """Test handle_async_mjpeg_stream.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + mock_request = make_mocked_request("GET", "/", headers={"token": "x"}) + stream = await async_get_mjpeg_stream( + hass, mock_request, "camera.my_camera_live_view" + ) + assert stream is not None + + +async def test_handle_mjpeg_stream_not_supported( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, +) -> None: + """Test no stream if stream_rtsp_url is None after creation.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + mock_camera = mock_device.modules[Module.Camera] + + mock_camera.stream_rtsp_url.side_effect = ("foo", None) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + mock_request = make_mocked_request("GET", "/", headers={"token": "x"}) + mjpeg_stream = await async_get_mjpeg_stream( + hass, mock_request, "camera.my_camera_live_view" + ) + assert mjpeg_stream is None + + +async def test_camera_image( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_get_image.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + with patch( + "homeassistant.components.ffmpeg.async_get_image", + return_value=SMALLEST_VALID_JPEG_BYTES, + ) as mock_get_image: + image = await async_get_image(hass, "camera.my_camera_live_view") + assert image + assert image.content == SMALLEST_VALID_JPEG_BYTES + mock_get_image.assert_called_once() + + mock_get_image.reset_mock() + image = await async_get_image(hass, "camera.my_camera_live_view") + mock_get_image.assert_not_called() + + freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + mock_get_image.reset_mock() + image = await async_get_image(hass, "camera.my_camera_live_view") + mock_get_image.assert_called_once() + + freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # Test image returns None + with patch( + "homeassistant.components.ffmpeg.async_get_image", + return_value=None, + ) as mock_get_image: + msg = f"None camera image returned for {IP_ADDRESS3}" + assert msg not in caplog.text + + mock_get_image.reset_mock() + image = await async_get_image(hass, "camera.my_camera_live_view") + mock_get_image.assert_called_once() + + assert msg in caplog.text + + +async def test_no_camera_image_when_streaming( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test no camera image when streaming.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + with patch( + "homeassistant.components.ffmpeg.async_get_image", + return_value=SMALLEST_VALID_JPEG_BYTES, + ) as mock_get_image: + await async_get_image(hass, "camera.my_camera_live_view") + mock_get_image.assert_called_once() + + freezer.tick(TPLinkCameraEntity.IMAGE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + start_event = asyncio.Event() + finish_event = asyncio.Event() + + async def _waiter(*_, **__): + start_event.set() + await finish_event.wait() + + async def _get_stream(): + mock_request = make_mocked_request("GET", "/", headers={"token": "x"}) + await async_get_mjpeg_stream( + hass, mock_request, "camera.my_camera_live_view" + ) + + mock_get_image.reset_mock() + with patch( + "homeassistant.components.tplink.camera.async_aiohttp_proxy_stream", + new=_waiter, + ): + task = asyncio.create_task(_get_stream()) + await start_event.wait() + await async_get_image(hass, "camera.my_camera_live_view") + finish_event.set() + await task + + mock_get_image.assert_not_called() + + +async def test_no_concurrent_camera_image( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, +) -> None: + """Test async_get_image doesn't make concurrent requests.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + finish_event = asyncio.Event() + call_count = 0 + + async def _waiter(*_, **__): + nonlocal call_count + call_count += 1 + await finish_event.wait() + return SMALLEST_VALID_JPEG_BYTES + + with patch( + "homeassistant.components.ffmpeg.async_get_image", + new=_waiter, + ): + tasks = asyncio.gather( + async_get_image(hass, "camera.my_camera_live_view"), + async_get_image(hass, "camera.my_camera_live_view"), + ) + # Sleep to give both tasks chance to get to th asyncio.Lock() + await asyncio.sleep(0) + finish_event.set() + results = await tasks + assert len(results) == 2 + assert all(img and img.content == SMALLEST_VALID_JPEG_BYTES for img in results) + assert call_count == 1 + + +async def test_camera_image_auth_error( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + mock_connect: AsyncMock, + mock_discovery: AsyncMock, +) -> None: + """Test async_get_image auth error.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + with ( + patch( + "homeassistant.components.ffmpeg.async_get_image", + return_value=b"", + ), + patch( + "homeassistant.components.stream.async_check_stream_client_error", + side_effect=stream.StreamOpenClientError( + "Request was unauthorized", + error_code=stream.StreamClientError.Unauthorized, + ), + ), + pytest.raises(HomeAssistantError), + ): + await async_get_image(hass, "camera.my_camera_live_view") + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + [result] = flows + + assert result["step_id"] == "camera_auth_confirm" + + +async def test_camera_stream_source( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test camera stream source. + + This test would fail if the integration didn't properly + put stream in the dependencies. + """ + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + client = await hass_ws_client(hass) + await client.send_json_auto_id( + {"type": "camera/stream", "entity_id": "camera.my_camera_live_view"} + ) + msg = await client.receive_json() + + # Assert WebSocket response + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert "url" in msg["result"] + + +async def test_camera_stream_attributes( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, +) -> None: + """Test stream attributes.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + supported_features = state.attributes.get("supported_features") + assert supported_features is CameraEntityFeature.STREAM | CameraEntityFeature.ON_OFF + camera = get_camera_from_entity_id(hass, "camera.my_camera_live_view") + assert camera.camera_capabilities.frontend_stream_types == {StreamType.HLS} + + +async def test_camera_turn_on_off( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, +) -> None: + """Test camera turn on and off.""" + mock_device = _mocked_device( + modules=[Module.Camera], + alias="my_camera", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + mock_camera = mock_device.modules[Module.Camera] + + await setup_platform_for_device( + hass, mock_camera_config_entry, Platform.CAMERA, mock_device + ) + + state = hass.states.get("camera.my_camera_live_view") + assert state is not None + + await hass.services.async_call( + CAMERA_DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "camera.my_camera_live_view"}, + blocking=True, + ) + mock_camera.set_state.assert_called_with(True) + + await hass.services.async_call( + CAMERA_DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "camera.my_camera_live_view"}, + blocking=True, + ) + mock_camera.set_state.assert_called_with(False) diff --git a/tests/components/tplink/test_climate.py b/tests/components/tplink/test_climate.py index 3a54048e1d6..adcca24886b 100644 --- a/tests/components/tplink/test_climate.py +++ b/tests/components/tplink/test_climate.py @@ -2,7 +2,7 @@ from datetime import timedelta -from kasa import Device, Feature +from kasa import Device, Feature, Module from kasa.smart.modules.temperaturecontrol import ThermostatState import pytest from syrupy.assertion import SnapshotAssertion @@ -27,12 +27,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from . import ( - DEVICE_ID, _mocked_device, _mocked_feature, setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID from tests.common import MockConfigEntry, async_fire_time_changed @@ -41,35 +41,28 @@ ENTITY_ID = "climate.thermostat" @pytest.fixture async def mocked_hub(hass: HomeAssistant) -> Device: - """Return mocked tplink binary sensor feature.""" + """Return mocked tplink hub.""" features = [ _mocked_feature( - "temperature", value=20.2, category=Feature.Category.Primary, unit="celsius" - ), - _mocked_feature( - "target_temperature", - value=22.2, + "temperature", type_=Feature.Type.Number, category=Feature.Category.Primary, unit="celsius", ), _mocked_feature( - "state", - value=True, - type_=Feature.Type.Switch, - category=Feature.Category.Primary, - ), - _mocked_feature( - "thermostat_mode", - value=ThermostatState.Heating, - type_=Feature.Type.Choice, + "target_temperature", + type_=Feature.Type.Number, category=Feature.Category.Primary, + unit="celsius", ), ] thermostat = _mocked_device( - alias="thermostat", features=features, device_type=Device.Type.Thermostat + alias="thermostat", + features=features, + modules=[Module.Thermostat], + device_type=Device.Type.Thermostat, ) return _mocked_device( @@ -121,7 +114,9 @@ async def test_set_temperature( ) -> None: """Test that set_temperature service calls the setter.""" mocked_thermostat = mocked_hub.children[0] - mocked_thermostat.features["target_temperature"].minimum_value = 0 + + therm_module = mocked_thermostat.modules.get(Module.Thermostat) + assert therm_module await setup_platform_for_device( hass, mock_config_entry, Platform.CLIMATE, mocked_hub @@ -133,8 +128,8 @@ async def test_set_temperature( {ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 10}, blocking=True, ) - target_temp_feature = mocked_thermostat.features["target_temperature"] - target_temp_feature.set_value.assert_called_with(10) + + therm_module.set_target_temperature.assert_called_with(10) async def test_set_hvac_mode( @@ -146,8 +141,8 @@ async def test_set_hvac_mode( ) mocked_thermostat = mocked_hub.children[0] - mocked_state = mocked_thermostat.features["state"] - assert mocked_state is not None + therm_module = mocked_thermostat.modules.get(Module.Thermostat) + assert therm_module await hass.services.async_call( CLIMATE_DOMAIN, @@ -156,7 +151,7 @@ async def test_set_hvac_mode( blocking=True, ) - mocked_state.set_value.assert_called_with(False) + therm_module.set_state.assert_called_with(False) await hass.services.async_call( CLIMATE_DOMAIN, @@ -164,9 +159,10 @@ async def test_set_hvac_mode( {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_HVAC_MODE: HVACMode.HEAT}, blocking=True, ) - mocked_state.set_value.assert_called_with(True) + therm_module.set_state.assert_called_with(True) - with pytest.raises(ServiceValidationError): + msg = "Tried to set unsupported mode: dry" + with pytest.raises(ServiceValidationError, match=msg): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, @@ -184,7 +180,8 @@ async def test_turn_on_and_off( ) mocked_thermostat = mocked_hub.children[0] - mocked_state = mocked_thermostat.features["state"] + therm_module = mocked_thermostat.modules.get(Module.Thermostat) + assert therm_module await hass.services.async_call( CLIMATE_DOMAIN, @@ -193,7 +190,7 @@ async def test_turn_on_and_off( blocking=True, ) - mocked_state.set_value.assert_called_with(False) + therm_module.set_state.assert_called_with(False) await hass.services.async_call( CLIMATE_DOMAIN, @@ -202,7 +199,7 @@ async def test_turn_on_and_off( blocking=True, ) - mocked_state.set_value.assert_called_with(True) + therm_module.set_state.assert_called_with(True) async def test_unknown_mode( @@ -217,11 +214,31 @@ async def test_unknown_mode( ) mocked_thermostat = mocked_hub.children[0] - mocked_state = mocked_thermostat.features["thermostat_mode"] - mocked_state.value = ThermostatState.Unknown + therm_module = mocked_thermostat.modules.get(Module.Thermostat) + assert therm_module + + therm_module.mode = ThermostatState.Unknown async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert "Unknown thermostat state, defaulting to OFF" in caplog.text + + +async def test_missing_feature_attributes( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_hub: Device, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that a module missing the min/max and unit feature logs an error.""" + mocked_thermostat = mocked_hub.children[0] + mocked_thermostat.features.pop("target_temperature") + mocked_thermostat.features.pop("temperature") + + await setup_platform_for_device( + hass, mock_config_entry, Platform.CLIMATE, mocked_hub + ) + assert "Unable to get min/max target temperature" in caplog.text + assert "Unable to get correct temperature unit" in caplog.text diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 2697696c667..35fd4f418de 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -1,14 +1,13 @@ """Test the tplink config flow.""" -from contextlib import contextmanager import logging from unittest.mock import ANY, AsyncMock, patch -from kasa import TimeoutError +from kasa import Module, TimeoutError import pytest from homeassistant import config_entries -from homeassistant.components import dhcp +from homeassistant.components import stream from homeassistant.components.tplink import ( DOMAIN, AuthenticationError, @@ -19,9 +18,11 @@ from homeassistant.components.tplink import ( ) from homeassistant.components.tplink.config_flow import TPLinkConfigFlow from homeassistant.components.tplink.const import ( + CONF_CAMERA_CREDENTIALS, CONF_CONNECTION_PARAMETERS, CONF_CREDENTIALS_HASH, CONF_DEVICE_CONFIG, + CONF_LIVE_VIEW, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( @@ -35,48 +36,45 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from . import ( +from . import _mocked_device, _patch_connect, _patch_discovery, _patch_single_discovery +from .conftest import override_side_effect +from .const import ( AES_KEYS, ALIAS, + ALIAS_CAMERA, CONN_PARAMS_AES, CONN_PARAMS_KLAP, CONN_PARAMS_LEGACY, CREATE_ENTRY_DATA_AES, + CREATE_ENTRY_DATA_AES_CAMERA, CREATE_ENTRY_DATA_KLAP, CREATE_ENTRY_DATA_LEGACY, CREDENTIALS_HASH_AES, CREDENTIALS_HASH_KLAP, DEFAULT_ENTRY_TITLE, + DEFAULT_ENTRY_TITLE_CAMERA, DEVICE_CONFIG_AES, + DEVICE_CONFIG_AES_CAMERA, DEVICE_CONFIG_DICT_KLAP, DEVICE_CONFIG_KLAP, DEVICE_CONFIG_LEGACY, DHCP_FORMATTED_MAC_ADDRESS, IP_ADDRESS, + IP_ADDRESS2, + IP_ADDRESS3, MAC_ADDRESS, MAC_ADDRESS2, + MAC_ADDRESS3, + MODEL_CAMERA, MODULE, - _mocked_device, - _patch_connect, - _patch_discovery, - _patch_single_discovery, + SMALLEST_VALID_JPEG_BYTES, ) from tests.common import MockConfigEntry -@contextmanager -def override_side_effect(mock: AsyncMock, effect): - """Temporarily override a mock side effect and replace afterwards.""" - try: - default_side_effect = mock.side_effect - mock.side_effect = effect - yield mock - finally: - mock.side_effect = default_side_effect - - @pytest.mark.parametrize( ("device_config", "expected_entry_data", "credentials_hash"), [ @@ -98,6 +96,7 @@ async def test_discovery( device_config=device_config, credentials_hash=credentials_hash, ip_address=ip_address, + mac=MAC_ADDRESS, ) with ( _patch_discovery(device, ip_address=ip_address), @@ -143,7 +142,7 @@ async def test_discovery( result["flow_id"], {CONF_DEVICE: MAC_ADDRESS}, ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == DEFAULT_ENTRY_TITLE @@ -167,13 +166,151 @@ async def test_discovery( assert result2["reason"] == "no_devices_found" +@pytest.mark.usefixtures("mock_init") +async def test_discovery_camera( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test authenticated discovery for camera with stream.""" + mock_device = _mocked_device( + alias=ALIAS_CAMERA, + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + model=MODEL_CAMERA, + device_config=DEVICE_CONFIG_AES_CAMERA, + credentials_hash=CREDENTIALS_HASH_AES, + modules=[Module.Camera], + ) + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS3, + CONF_MAC: MAC_ADDRESS3, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mock_device, + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert not result["errors"] + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert not result["errors"] + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_ENTRY_TITLE_CAMERA + assert result["data"] == CREATE_ENTRY_DATA_AES_CAMERA + assert result["context"]["unique_id"] == MAC_ADDRESS3 + + +@pytest.mark.usefixtures("mock_init") +async def test_discovery_pick_device_camera( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test authenticated discovery for camera with stream.""" + mock_device = _mocked_device( + alias=ALIAS_CAMERA, + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + model=MODEL_CAMERA, + device_config=DEVICE_CONFIG_AES_CAMERA, + credentials_hash=CREDENTIALS_HASH_AES, + modules=[Module.Camera], + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + with override_side_effect( + mock_discovery["discover"], lambda *_, **__: {IP_ADDRESS3: mock_device} + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "pick_device" + assert not result["errors"] + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_DEVICE: MAC_ADDRESS3}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert not result["errors"] + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_ENTRY_TITLE_CAMERA + assert result["data"] == CREATE_ENTRY_DATA_AES_CAMERA + assert result["context"]["unique_id"] == MAC_ADDRESS3 + + +@pytest.mark.usefixtures("mock_init") async def test_discovery_auth( - hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, mock_init + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, ) -> None: """Test authenticated discovery.""" - - mock_device = mock_connect["mock_devices"][IP_ADDRESS] - assert mock_device.config == DEVICE_CONFIG_KLAP + mock_device = _mocked_device( + alias=ALIAS, + ip_address=IP_ADDRESS, + mac=MAC_ADDRESS, + device_config=DEVICE_CONFIG_KLAP, + credentials_hash=CREDENTIALS_HASH_KLAP, + ) with override_side_effect(mock_connect["connect"], AuthenticationError): result = await hass.config_entries.flow.async_init( @@ -191,13 +328,14 @@ async def test_discovery_auth( assert result["step_id"] == "discovery_auth_confirm" assert not result["errors"] - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_ENTRY_TITLE @@ -205,6 +343,72 @@ async def test_discovery_auth( assert result2["context"]["unique_id"] == MAC_ADDRESS +@pytest.mark.usefixtures("mock_init") +async def test_discovery_auth_camera( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test authenticated discovery for camera with stream.""" + mock_device = _mocked_device( + alias=ALIAS_CAMERA, + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + model=MODEL_CAMERA, + device_config=DEVICE_CONFIG_AES_CAMERA, + credentials_hash=CREDENTIALS_HASH_AES, + modules=[Module.Camera], + ) + + with override_side_effect(mock_connect["connect"], AuthenticationError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: IP_ADDRESS3, + CONF_MAC: MAC_ADDRESS3, + CONF_ALIAS: ALIAS, + CONF_DEVICE: mock_device, + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_auth_confirm" + assert not result["errors"] + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert not result["errors"] + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_ENTRY_TITLE_CAMERA + assert result["data"] == CREATE_ENTRY_DATA_AES_CAMERA + assert result["context"]["unique_id"] == MAC_ADDRESS3 + + @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ @@ -213,13 +417,13 @@ async def test_discovery_auth( ], ids=["invalid-auth", "unknown-error"], ) +@pytest.mark.usefixtures("mock_init") async def test_discovery_auth_errors( hass: HomeAssistant, mock_connect: AsyncMock, - mock_init, - error_type, - errors_msg, - error_placement, + error_type: Exception, + errors_msg: str, + error_placement: str, ) -> None: """Test handling of discovery authentication errors. @@ -271,10 +475,10 @@ async def test_discovery_auth_errors( assert result3["context"]["unique_id"] == MAC_ADDRESS +@pytest.mark.usefixtures("mock_init") async def test_discovery_new_credentials( hass: HomeAssistant, mock_connect: AsyncMock, - mock_init, ) -> None: """Test setting up discovery with new credentials.""" mock_device = mock_connect["mock_devices"][IP_ADDRESS] @@ -320,10 +524,10 @@ async def test_discovery_new_credentials( assert result3["context"]["unique_id"] == MAC_ADDRESS +@pytest.mark.usefixtures("mock_init") async def test_discovery_new_credentials_invalid( hass: HomeAssistant, mock_connect: AsyncMock, - mock_init, ) -> None: """Test setting up discovery with new invalid credentials.""" mock_device = mock_connect["mock_devices"][IP_ADDRESS] @@ -385,7 +589,7 @@ async def test_discovery_new_credentials_invalid( async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> None: """Test setting up discovery.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id="dd:dd:dd:dd:dd:dd" + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS2}, unique_id="dd:dd:dd:dd:dd:dd" ) config_entry.add_to_hass(hass) @@ -535,6 +739,228 @@ async def test_manual(hass: HomeAssistant) -> None: assert result2["reason"] == "already_configured" +async def test_manual_camera( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test manual camera.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS3} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + + # Test no username or pass + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert result["errors"] == {"base": "camera_creds"} + + # Test unknown error + with ( + patch( + "homeassistant.components.stream.async_check_stream_client_error", + side_effect=stream.StreamOpenClientError( + "Stream was not found", error_code=stream.StreamClientError.NotFound + ), + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert result["errors"] == {"base": "cannot_connect_camera"} + assert "error" in result["description_placeholders"] + + # Test unknown error + with ( + patch( + "homeassistant.components.stream.async_check_stream_client_error", + side_effect=stream.StreamOpenClientError( + "Request is unauthorized", + error_code=stream.StreamClientError.Unauthorized, + ), + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert result["errors"] == {"base": "invalid_camera_auth"} + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_CAMERA_CREDENTIALS] == { + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + } + assert result["data"][CONF_LIVE_VIEW] is True + + +@pytest.mark.parametrize( + "exception", + [ + pytest.param( + stream.StreamOpenClientError( + "Stream was not found", error_code=stream.StreamClientError.NotFound + ), + id="open_client_error", + ), + pytest.param(Exception(), id="other_error"), + ], +) +async def test_manual_camera_no_hls( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, + exception: Exception, +) -> None: + """Test manual camera when hls stream fails but mpeg stream works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS3} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + + # Test stream error + with ( + patch( + "homeassistant.components.stream.async_check_stream_client_error", + side_effect=exception, + ), + patch("homeassistant.components.ffmpeg.async_get_image", return_value=None), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + assert result["errors"] == {"base": "cannot_connect_camera"} + assert "error" in result["description_placeholders"] + + # async_get_image will succeed + with ( + patch( + "homeassistant.components.stream.async_check_stream_client_error", + side_effect=exception, + ), + patch( + "homeassistant.components.ffmpeg.async_get_image", + return_value=SMALLEST_VALID_JPEG_BYTES, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_CAMERA_CREDENTIALS] == { + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + } + assert result["data"][CONF_LIVE_VIEW] is True + + +async def test_manual_camera_no_live_view( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test manual camera.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS3} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: False, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert CONF_CAMERA_CREDENTIALS not in result["data"] + assert result["data"][CONF_LIVE_VIEW] is False + + async def test_manual_no_capabilities(hass: HomeAssistant) -> None: """Test manually setup without successful get_capabilities.""" result = await hass.config_entries.flow.async_init( @@ -561,11 +987,11 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: assert result["context"]["unique_id"] == MAC_ADDRESS +@pytest.mark.usefixtures("mock_init") async def test_manual_auth( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, - mock_init, ) -> None: """Test manually setup.""" result = await hass.config_entries.flow.async_init( @@ -575,7 +1001,7 @@ async def test_manual_auth( assert result["step_id"] == "user" assert not result["errors"] - mock_discovery["mock_device"].update.side_effect = AuthenticationError + mock_discovery["mock_devices"][IP_ADDRESS].update.side_effect = AuthenticationError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: IP_ADDRESS} @@ -586,7 +1012,7 @@ async def test_manual_auth( assert result2["step_id"] == "user_auth_confirm" assert not result2["errors"] - mock_discovery["mock_device"].update.reset_mock(side_effect=True) + mock_discovery["mock_devices"][IP_ADDRESS].update.reset_mock(side_effect=True) result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -602,6 +1028,63 @@ async def test_manual_auth( assert result3["context"]["unique_id"] == MAC_ADDRESS +async def test_manual_auth_camera( + hass: HomeAssistant, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test manual camera.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + with override_side_effect( + mock_discovery["mock_devices"][IP_ADDRESS3].update, AuthenticationError + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS3} + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user_auth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "foobar", + CONF_PASSWORD: "foobar", + }, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_CAMERA_CREDENTIALS] == { + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + } + assert result["data"][CONF_LIVE_VIEW] is True + + @pytest.mark.parametrize( ("error_type", "errors_msg", "error_placement"), [ @@ -610,14 +1093,14 @@ async def test_manual_auth( ], ids=["invalid-auth", "unknown-error"], ) +@pytest.mark.usefixtures("mock_init") async def test_manual_auth_errors( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, - mock_init, - error_type, - errors_msg, - error_placement, + error_type: Exception, + errors_msg: str, + error_placement: str, ) -> None: """Test manually setup auth errors.""" result = await hass.config_entries.flow.async_init( @@ -627,7 +1110,7 @@ async def test_manual_auth_errors( assert result["step_id"] == "user" assert not result["errors"] - mock_discovery["mock_device"].update.side_effect = AuthenticationError + mock_discovery["mock_devices"][IP_ADDRESS].update.side_effect = AuthenticationError with override_side_effect(mock_connect["connect"], error_type): result2 = await hass.config_entries.flow.async_configure( @@ -677,16 +1160,31 @@ async def test_manual_port_override( hass: HomeAssistant, mock_connect: AsyncMock, mock_discovery: AsyncMock, - host_str, - host, - port, + host_str: str, + host: str, + port: int, ) -> None: """Test manually setup.""" - mock_discovery["mock_device"].config.port_override = port - mock_discovery["mock_device"].host = host - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + config = DeviceConfig( + host, + credentials=None, + port_override=port, + connection_type=CONN_PARAMS_KLAP, ) + mock_device = _mocked_device( + alias=ALIAS, + ip_address=host, + mac=MAC_ADDRESS, + device_config=config, + credentials_hash=CREDENTIALS_HASH_KLAP, + ) + + with override_side_effect( + mock_discovery["try_connect_all"], lambda *_, **__: mock_device + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -696,23 +1194,29 @@ async def test_manual_port_override( mock_discovery["discover_single"].side_effect = TimeoutError mock_connect["connect"].side_effect = AuthenticationError - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: host_str} - ) - await hass.async_block_till_done() + with override_side_effect( + mock_discovery["try_connect_all"], lambda *_, **__: mock_device + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: host_str} + ) + await hass.async_block_till_done() assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user_auth_confirm" assert not result2["errors"] creds = Credentials("fake_username", "fake_password") - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - user_input={ - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) + with override_side_effect( + mock_discovery["try_connect_all"], lambda *_, **__: mock_device + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) await hass.async_block_till_done() mock_discovery["try_connect_all"].assert_called_once_with( host, credentials=creds, port=port, http_client=ANY @@ -744,7 +1248,7 @@ async def test_manual_port_override_invalid( await hass.async_block_till_done() mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=None, port=None + IP_ADDRESS, credentials=None, port=None ) assert result2["type"] is FlowResultType.CREATE_ENTRY @@ -787,7 +1291,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ) @@ -801,7 +1305,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=IP_ADDRESS, macaddress="000000000000", hostname="mock_hostname" ), ) @@ -817,7 +1321,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.5", macaddress="000000000001", hostname="mock_hostname" ), ) @@ -831,7 +1335,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ), @@ -847,7 +1351,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ], ) async def test_discovered_by_dhcp_or_discovery( - hass: HomeAssistant, source, data + hass: HomeAssistant, source: str, data: dict ) -> None: """Test we can setup when discovered from dhcp or discovery.""" @@ -885,7 +1389,7 @@ async def test_discovered_by_dhcp_or_discovery( [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ), @@ -901,7 +1405,7 @@ async def test_discovered_by_dhcp_or_discovery( ], ) async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( - hass: HomeAssistant, source, data + hass: HomeAssistant, source: str, data: dict ) -> None: """Test we abort if we cannot get the unique id when discovered from dhcp.""" @@ -924,7 +1428,7 @@ async def test_integration_discovery_with_ip_change( mock_discovery: AsyncMock, mock_connect: AsyncMock, ) -> None: - """Test reauth flow.""" + """Test integration updates ip address from discovery.""" mock_config_entry.add_to_hass(hass) with ( patch("homeassistant.components.tplink.Discover.discover", return_value={}), @@ -941,7 +1445,7 @@ async def test_integration_discovery_with_ip_change( mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_LEGACY.to_dict() ) - assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS mocked_device = _mocked_device(device_config=DEVICE_CONFIG_KLAP) with override_side_effect(mock_connect["connect"], lambda *_, **__: mocked_device): @@ -949,7 +1453,7 @@ async def test_integration_discovery_with_ip_change( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={ - CONF_HOST: "127.0.0.2", + CONF_HOST: IP_ADDRESS2, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, CONF_DEVICE: mocked_device, @@ -961,7 +1465,7 @@ async def test_integration_discovery_with_ip_change( assert ( mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() ) - assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS2 config = DeviceConfig.from_dict(DEVICE_CONFIG_DICT_KLAP) @@ -984,9 +1488,8 @@ async def test_integration_discovery_with_ip_change( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED # Check that init set the new host correctly before calling connect - assert config.host == "127.0.0.1" - config.host = "127.0.0.2" - config.uses_http = False # Not passed in to new config class + assert config.host == IP_ADDRESS + config.host = IP_ADDRESS2 config.http_client = "Foo" mock_connect["connect"].assert_awaited_once_with(config=config) @@ -1024,7 +1527,7 @@ async def test_integration_discovery_with_connection_change( ) == 0 ) - assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS2 assert ( mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_AES.to_dict() ) @@ -1034,7 +1537,7 @@ async def test_integration_discovery_with_connection_change( NEW_DEVICE_CONFIG = { **DEVICE_CONFIG_DICT_KLAP, "connection_type": CONN_PARAMS_KLAP.to_dict(), - CONF_HOST: "127.0.0.2", + CONF_HOST: IP_ADDRESS2, } config = DeviceConfig.from_dict(NEW_DEVICE_CONFIG) # Reset the connect mock so when the config flow reloads the entry it succeeds @@ -1055,7 +1558,7 @@ async def test_integration_discovery_with_connection_change( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={ - CONF_HOST: "127.0.0.2", + CONF_HOST: IP_ADDRESS2, CONF_MAC: MAC_ADDRESS2, CONF_ALIAS: ALIAS, CONF_DEVICE: bulb, @@ -1067,13 +1570,12 @@ async def test_integration_discovery_with_connection_change( assert ( mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() ) - assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS2 assert CREDENTIALS_HASH_AES not in mock_config_entry.data assert mock_config_entry.state is ConfigEntryState.LOADED - config.host = "127.0.0.2" - config.uses_http = False # Not passed in to new config class + config.host = IP_ADDRESS2 config.http_client = "Foo" config.aes_keys = AES_KEYS mock_connect["connect"].assert_awaited_once_with(config=config) @@ -1097,18 +1599,18 @@ async def test_dhcp_discovery_with_ip_change( flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 - assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS discovery_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip="127.0.0.2", macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + data=DhcpServiceInfo( + ip=IP_ADDRESS2, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ) assert discovery_result["type"] is FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" - assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS2 async def test_dhcp_discovery_discover_fail( @@ -1121,14 +1623,14 @@ async def test_dhcp_discovery_discover_fail( flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 - assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS with override_side_effect(mock_discovery["discover_single"], TimeoutError): discovery_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip="127.0.0.2", macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS + data=DhcpServiceInfo( + ip=IP_ADDRESS2, macaddress=DHCP_FORMATTED_MAC_ADDRESS, hostname=ALIAS ), ) assert discovery_result["type"] is FlowResultType.ABORT @@ -1160,15 +1662,58 @@ async def test_reauth( ) credentials = Credentials("fake_username", "fake_password") mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=credentials, port=None + IP_ADDRESS, credentials=credentials, port=None ) - mock_discovery["mock_device"].update.assert_called_once_with() + mock_discovery["mock_devices"][IP_ADDRESS].update.assert_called_once_with() assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" await hass.async_block_till_done() +async def test_reauth_camera( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test reauth flow on invalid camera credentials.""" + mock_device = mock_connect["mock_devices"][IP_ADDRESS3] + mock_camera_config_entry.add_to_hass(hass) + mock_camera_config_entry.async_start_reauth( + hass, + config_entries.ConfigFlowContext( + reauth_source=CONF_CAMERA_CREDENTIALS, # type: ignore[typeddict-unknown-key] + ), + {"device": mock_device}, + ) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + [result] = flows + + assert result["step_id"] == "camera_auth_confirm" + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser2", + CONF_PASSWORD: "campass2", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert dict(mock_camera_config_entry.data) == { + **CREATE_ENTRY_DATA_AES_CAMERA, + CONF_CAMERA_CREDENTIALS: {CONF_USERNAME: "camuser2", CONF_PASSWORD: "campass2"}, + } + + async def test_reauth_try_connect_all( hass: HomeAssistant, mock_added_config_entry: MockConfigEntry, @@ -1195,7 +1740,7 @@ async def test_reauth_try_connect_all( ) credentials = Credentials("fake_username", "fake_password") mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=credentials, port=None + IP_ADDRESS, credentials=credentials, port=None ) mock_discovery["try_connect_all"].assert_called_once() assert result2["type"] is FlowResultType.ABORT @@ -1224,7 +1769,7 @@ async def test_reauth_try_connect_all_fail( override_side_effect(mock_discovery["discover_single"], TimeoutError), override_side_effect(mock_discovery["try_connect_all"], lambda *_, **__: None), ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ CONF_USERNAME: "fake_username", @@ -1233,10 +1778,26 @@ async def test_reauth_try_connect_all_fail( ) credentials = Credentials("fake_username", "fake_password") mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=credentials, port=None + IP_ADDRESS, credentials=credentials, port=None ) mock_discovery["try_connect_all"].assert_called_once() - assert result2["errors"] == {"base": "cannot_connect"} + assert result["errors"] == {"base": "cannot_connect"} + + mock_discovery["try_connect_all"].reset_mock() + with ( + override_side_effect(mock_discovery["discover_single"], TimeoutError), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + + mock_discovery["try_connect_all"].assert_called_once() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" async def test_reauth_update_with_encryption_change( @@ -1278,40 +1839,47 @@ async def test_reauth_update_with_encryption_change( assert CONF_CREDENTIALS_HASH not in mock_config_entry.data new_config = DeviceConfig( - "127.0.0.2", + IP_ADDRESS2, credentials=None, connection_type=Device.ConnectionParameters( Device.Family.SmartTapoPlug, Device.EncryptionType.Klap ), - uses_http=True, ) - mock_discovery["mock_device"].host = "127.0.0.2" - mock_discovery["mock_device"].config = new_config - mock_discovery["mock_device"].credentials_hash = None - mock_connect["mock_devices"]["127.0.0.2"].config = new_config - mock_connect["mock_devices"]["127.0.0.2"].credentials_hash = CREDENTIALS_HASH_KLAP + mock_device = _mocked_device( + alias="my_device", + ip_address=IP_ADDRESS2, + mac=MAC_ADDRESS2, + device_config=new_config, + credentials_hash=CREDENTIALS_HASH_KLAP, + ) - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) - await hass.async_block_till_done(wait_background_tasks=True) + with ( + override_side_effect( + mock_discovery["discover_single"], lambda *_, **__: mock_device + ), + override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + await hass.async_block_till_done(wait_background_tasks=True) assert "Connection type changed for 127.0.0.2" in caplog.text credentials = Credentials("fake_username", "fake_password") mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.2", credentials=credentials, port=None + IP_ADDRESS2, credentials=credentials, port=None ) - mock_discovery["mock_device"].update.assert_called_once_with() + mock_device.update.assert_called_once_with() assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config_entry.state is ConfigEntryState.LOADED assert ( mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() ) - assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS2 assert mock_config_entry.data[CONF_CREDENTIALS_HASH] == CREDENTIALS_HASH_KLAP @@ -1398,7 +1966,7 @@ async def test_reauth_update_from_discovery_with_ip_change( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={ - CONF_HOST: "127.0.0.2", + CONF_HOST: IP_ADDRESS2, CONF_MAC: MAC_ADDRESS, CONF_ALIAS: ALIAS, CONF_DEVICE: device, @@ -1410,7 +1978,7 @@ async def test_reauth_update_from_discovery_with_ip_change( assert ( mock_config_entry.data[CONF_CONNECTION_PARAMETERS] == CONN_PARAMS_KLAP.to_dict() ) - assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + assert mock_config_entry.data[CONF_HOST] == IP_ADDRESS2 async def test_reauth_no_update_if_config_and_ip_the_same( @@ -1479,9 +2047,9 @@ async def test_reauth_errors( mock_added_config_entry: MockConfigEntry, mock_discovery: AsyncMock, mock_connect: AsyncMock, - error_type, - errors_msg, - error_placement, + error_type: Exception, + errors_msg: str, + error_placement: str, ) -> None: """Test reauth errors.""" mock_added_config_entry.async_start_reauth(hass) @@ -1493,26 +2061,27 @@ async def test_reauth_errors( [result] = flows assert result["step_id"] == "reauth_confirm" - mock_discovery["mock_device"].update.side_effect = error_type - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) + mock_device = mock_discovery["mock_devices"][IP_ADDRESS] + with override_side_effect(mock_device.update, error_type): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) credentials = Credentials("fake_username", "fake_password") mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=credentials, port=None + IP_ADDRESS, credentials=credentials, port=None ) - mock_discovery["mock_device"].update.assert_called_once_with() + mock_device.update.assert_called_once_with() assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {error_placement: errors_msg} assert result2["description_placeholders"]["error"] == str(error_type) mock_discovery["discover_single"].reset_mock() - mock_discovery["mock_device"].update.reset_mock(side_effect=True) + mock_device.update.reset_mock(side_effect=True) result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={ @@ -1522,9 +2091,9 @@ async def test_reauth_errors( ) mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=credentials, port=None + IP_ADDRESS, credentials=credentials, port=None ) - mock_discovery["mock_device"].update.assert_called_once_with() + mock_device.update.assert_called_once_with() assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -1542,8 +2111,8 @@ async def test_pick_device_errors( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, - error_type, - expected_flow, + error_type: type[Exception], + expected_flow: FlowResultType, ) -> None: """Test errors on pick_device.""" result = await hass.config_entries.flow.async_init( @@ -1580,11 +2149,11 @@ async def test_pick_device_errors( assert result4["context"]["unique_id"] == MAC_ADDRESS +@pytest.mark.usefixtures("mock_init") async def test_discovery_timeout_try_connect_all( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, - mock_init, ) -> None: """Test discovery tries legacy connect on timeout.""" result = await hass.config_entries.flow.async_init( @@ -1606,11 +2175,11 @@ async def test_discovery_timeout_try_connect_all( assert mock_connect["connect"].call_count == 1 +@pytest.mark.usefixtures("mock_init") async def test_discovery_timeout_try_connect_all_needs_creds( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, - mock_init, ) -> None: """Test discovery tries legacy connect on timeout.""" result = await hass.config_entries.flow.async_init( @@ -1644,11 +2213,11 @@ async def test_discovery_timeout_try_connect_all_needs_creds( assert mock_connect["connect"].call_count == 1 +@pytest.mark.usefixtures("mock_init") async def test_discovery_timeout_try_connect_all_fail( hass: HomeAssistant, mock_discovery: AsyncMock, mock_connect: AsyncMock, - mock_init, ) -> None: """Test discovery tries legacy connect on timeout.""" result = await hass.config_entries.flow.async_init( @@ -1731,12 +2300,169 @@ async def test_reauth_update_other_flows( ) credentials = Credentials("fake_username", "fake_password") mock_discovery["discover_single"].assert_called_once_with( - "127.0.0.1", credentials=credentials, port=None + IP_ADDRESS, credentials=credentials, port=None ) - mock_discovery["mock_device"].update.assert_called_once_with() + mock_discovery["mock_devices"][IP_ADDRESS].update.assert_called_once_with() assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 0 + + +async def test_reconfigure( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test reconfigure flow.""" + result = await mock_added_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: IP_ADDRESS, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_auth_discovered( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test reconfigure auth flow for device that's discovered.""" + result = await mock_added_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # Simulate a bad host + with ( + override_side_effect( + mock_discovery["mock_devices"][IP_ADDRESS].update, KasaException + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "WRONG_IP", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": "cannot_connect"} + assert "error" in result["description_placeholders"] + + with ( + override_side_effect( + mock_discovery["mock_devices"][IP_ADDRESS].update, AuthenticationError + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: IP_ADDRESS, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user_auth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_auth_try_connect_all( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test reconfigure auth flow for device that's not discovered.""" + result = await mock_added_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + with ( + override_side_effect(mock_discovery["discover_single"], TimeoutError), + override_side_effect(mock_connect["connect"], KasaException), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: IP_ADDRESS, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user_auth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_camera( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + mock_discovery: AsyncMock, + mock_connect: AsyncMock, +) -> None: + """Test reconfigure flow.""" + mock_camera_config_entry.add_to_hass(hass) + result = await mock_camera_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: IP_ADDRESS3, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "camera_auth_confirm" + + with patch( + "homeassistant.components.stream.async_check_stream_client_error", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LIVE_VIEW: True, + CONF_USERNAME: "camuser", + CONF_PASSWORD: "campass", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" diff --git a/tests/components/tplink/test_fan.py b/tests/components/tplink/test_fan.py index deba33abfa5..13a768f683c 100644 --- a/tests/components/tplink/test_fan.py +++ b/tests/components/tplink/test_fan.py @@ -2,8 +2,7 @@ from __future__ import annotations -from datetime import timedelta - +from freezegun.api import FrozenDateTimeFactory from kasa import Device, Module from syrupy.assertion import SnapshotAssertion @@ -11,13 +10,15 @@ from homeassistant.components.fan import ( ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN, SERVICE_SET_PERCENTAGE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util -from . import DEVICE_ID, _mocked_device, setup_platform_for_device, snapshot_platform +from . import _mocked_device, setup_platform_for_device, snapshot_platform +from .const import DEVICE_ID from tests.common import MockConfigEntry, async_fire_time_changed @@ -56,6 +57,7 @@ async def test_fan_unique_id( hass: HomeAssistant, mock_config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test a fan unique id.""" fan = _mocked_device(modules=[Module.Fan], alias="my_fan") @@ -66,12 +68,16 @@ async def test_fan_unique_id( ) assert device_entries entity_id = "fan.my_fan" - entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == DEVICE_ID -async def test_fan(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> None: - """Test a color fan and that all transitions are correctly passed.""" +async def test_fan( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test fan functionality.""" device = _mocked_device(modules=[Module.Fan], alias="my_fan") fan = device.modules[Module.Fan] fan.fan_speed_level = 0 @@ -83,26 +89,29 @@ async def test_fan(hass: HomeAssistant, mock_config_entry: MockConfigEntry) -> N assert state.state == "off" await hass.services.async_call( - FAN_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) fan.set_fan_speed_level.assert_called_once_with(4) fan.set_fan_speed_level.reset_mock() fan.fan_speed_level = 4 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + + freezer.tick(10) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == "on" await hass.services.async_call( - FAN_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) fan.set_fan_speed_level.assert_called_once_with(0) fan.set_fan_speed_level.reset_mock() await hass.services.async_call( FAN_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 50}, blocking=True, ) diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 766e6784c8b..972ca73c45c 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -8,10 +8,18 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from kasa import AuthenticationError, DeviceConfig, Feature, KasaException, Module +from kasa import ( + AuthenticationError, + Device, + DeviceConfig, + DeviceType, + Feature, + KasaException, + Module, +) +from kasa.iot import IotStrip import pytest -from homeassistant import setup from homeassistant.components import tplink from homeassistant.components.tplink.const import ( CONF_AES_KEYS, @@ -38,6 +46,14 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import ( + _mocked_device, + _mocked_feature, + _patch_connect, + _patch_discovery, + _patch_single_discovery, +) +from .conftest import override_side_effect +from .const import ( ALIAS, CREATE_ENTRY_DATA_AES, CREATE_ENTRY_DATA_KLAP, @@ -51,12 +67,10 @@ from . import ( DEVICE_ID, DEVICE_ID_MAC, IP_ADDRESS, + IP_ADDRESS3, MAC_ADDRESS, + MAC_ADDRESS3, MODEL, - _mocked_device, - _patch_connect, - _patch_discovery, - _patch_single_discovery, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -69,6 +83,7 @@ async def test_configuring_tplink_causes_discovery( with ( patch("homeassistant.components.tplink.Discover.discover") as discover, patch("homeassistant.components.tplink.Discover.discover_single"), + patch("homeassistant.components.tplink.Device.connect"), ): discover.return_value = {MagicMock(): MagicMock()} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -95,7 +110,7 @@ async def test_config_entry_reload(hass: HomeAssistant) -> None: ) already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(), _patch_single_discovery(), _patch_connect(): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() assert already_migrated_config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) @@ -114,7 +129,7 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: _patch_single_discovery(no_device=True), _patch_connect(no_device=True), ): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -148,7 +163,7 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( _patch_single_discovery(device=dimmer), _patch_connect(device=dimmer), ): - await setup.async_setup_component(hass, DOMAIN, {}) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) migrated_dimmer_entity_reg = entity_registry.async_get_or_create( @@ -172,7 +187,7 @@ async def test_config_entry_wrong_mac_Address( ) already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(), _patch_single_discovery(), _patch_connect(): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() assert already_migrated_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -220,14 +235,17 @@ async def test_config_entry_with_stored_credentials( hass.data.setdefault(DOMAIN, {})[CONF_AUTHENTICATION] = auth mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.tplink.async_create_clientsession", return_value="Foo" + with ( + patch( + "homeassistant.components.tplink.async_create_clientsession", + return_value="Foo", + ), + override_side_effect(mock_discovery["discover"], lambda *_, **__: {}), ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.LOADED config = DeviceConfig.from_dict(DEVICE_CONFIG_KLAP.to_dict()) - config.uses_http = False config.http_client = "Foo" assert config.credentials != stored_credentials config.credentials = stored_credentials @@ -302,7 +320,7 @@ async def test_plug_auth_fails(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) device = _mocked_device(alias="my_plug", features=["state"]) with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_plug" @@ -335,13 +353,20 @@ async def test_update_attrs_fails_in_init( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) config_entry.add_to_hass(hass) - light = _mocked_device(modules=[Module.Light], alias="my_light") + features = [ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ] + light = _mocked_device(modules=[Module.Light], alias="my_light", features=features) light_module = light.modules[Module.Light] p = PropertyMock(side_effect=KasaException) type(light_module).color_temp = p light.__str__ = lambda _: "MockLight" with _patch_discovery(device=light), _patch_connect(device=light): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -349,7 +374,7 @@ async def test_update_attrs_fails_in_init( assert entity state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE - assert "Unable to read data for MockLight None:" in caplog.text + assert f"Unable to read data for MockLight {entity_id}:" in caplog.text async def test_update_attrs_fails_on_update( @@ -363,11 +388,18 @@ async def test_update_attrs_fails_on_update( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) config_entry.add_to_hass(hass) - light = _mocked_device(modules=[Module.Light], alias="my_light") + features = [ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ] + light = _mocked_device(modules=[Module.Light], alias="my_light", features=features) light_module = light.modules[Module.Light] with _patch_discovery(device=light), _patch_connect(device=light): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -413,7 +445,7 @@ async def test_feature_no_category( ) dev.features["led"].category = Feature.Category.Unset with _patch_discovery(device=dev), _patch_connect(device=dev): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_plug_led" @@ -480,7 +512,7 @@ async def test_unlink_devices( # Generate list of test identifiers test_identifiers = [ - (domain, f"{device_id}{"" if i == 0 else f"_000{i}"}") + (domain, f"{device_id}{'' if i == 0 else f'_000{i}'}") for i in range(id_count) for domain in domains ] @@ -729,7 +761,6 @@ async def test_credentials_hash_auth_error( expected_config = DeviceConfig.from_dict( {**DEVICE_CONFIG_DICT_KLAP, "credentials_hash": "theHash"} ) - expected_config.uses_http = False expected_config.http_client = "Foo" connect_mock.assert_called_with(config=expected_config) assert entry.state is ConfigEntryState.SETUP_ERROR @@ -761,13 +792,20 @@ async def test_migrate_remove_device_config( As async_setup_entry will succeed the hash on the parent is updated from the device. """ + old_device_config = { + k: v for k, v in device_config.to_dict().items() if k != "credentials" + } + device_config_dict = { + **old_device_config, + "uses_http": device_config.connection_type.encryption_type + is not Device.EncryptionType.Xor, + } + OLD_CREATE_ENTRY_DATA = { CONF_HOST: expected_entry_data[CONF_HOST], CONF_ALIAS: ALIAS, CONF_MODEL: MODEL, - CONF_DEVICE_CONFIG: { - k: v for k, v in device_config.to_dict().items() if k != "credentials" - }, + CONF_DEVICE_CONFIG: device_config_dict, } entry = MockConfigEntry( @@ -804,3 +842,390 @@ async def test_migrate_remove_device_config( assert entry.data == expected_entry_data assert "Migration to version 1.5 complete" in caplog.text + + +@pytest.mark.parametrize( + ("parent_device_type"), + [ + (Device), + (IotStrip), + ], +) +@pytest.mark.parametrize( + ("platform", "feature_id", "translated_name"), + [ + pytest.param("switch", "led", "led", id="switch"), + pytest.param( + "sensor", "current_consumption", "current_consumption", id="sensor" + ), + pytest.param("binary_sensor", "overheated", "overheated", id="binary_sensor"), + pytest.param("number", "smooth_transition_on", "smooth_on", id="number"), + pytest.param("select", "light_preset", "light_preset", id="select"), + pytest.param("button", "reboot", "restart", id="button"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_automatic_feature_device_addition_and_removal( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_connect: AsyncMock, + mock_discovery: AsyncMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, + platform: str, + feature_id: str, + translated_name: str, + parent_device_type: type, +) -> None: + """Test for automatic device with features addition and removal.""" + + children = { + f"child{index}": _mocked_device( + alias=f"child {index}", + features=[feature_id], + device_type=DeviceType.StripSocket, + device_id=f"child{index}", + ) + for index in range(1, 5) + } + + mock_device = _mocked_device( + alias="hub", + children=[children["child1"], children["child2"]], + features=[feature_id], + device_type=DeviceType.Hub, + spec=parent_device_type, + device_id="hub_parent", + ) + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + for child_id in (1, 2): + entity_id = f"{platform}.child_{child_id}_{translated_name}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + parent_device = device_registry.async_get_device( + identifiers={(DOMAIN, "hub_parent")} + ) + assert parent_device + + for device_id in ("child1", "child2"): + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + assert device_entry + assert device_entry.via_device_id == parent_device.id + + # Remove one of the devices + mock_device.children = [children["child1"]] + freezer.tick(5) + async_fire_time_changed(hass) + + entity_id = f"{platform}.child_2_{translated_name}" + state = hass.states.get(entity_id) + assert state is None + assert entity_registry.async_get(entity_id) is None + + assert device_registry.async_get_device(identifiers={(DOMAIN, "child2")}) is None + + # Re-dd the previously removed child device + mock_device.children = [ + children["child1"], + children["child2"], + ] + freezer.tick(5) + async_fire_time_changed(hass) + + for child_id in (1, 2): + entity_id = f"{platform}.child_{child_id}_{translated_name}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + for device_id in ("child1", "child2"): + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + assert device_entry + assert device_entry.via_device_id == parent_device.id + + # Add child devices + mock_device.children = [children["child1"], children["child3"], children["child4"]] + freezer.tick(5) + async_fire_time_changed(hass) + + for child_id in (1, 3, 4): + entity_id = f"{platform}.child_{child_id}_{translated_name}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + for device_id in ("child1", "child3", "child4"): + assert device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + + # Add the previously removed child device + mock_device.children = [ + children["child1"], + children["child2"], + children["child3"], + children["child4"], + ] + freezer.tick(5) + async_fire_time_changed(hass) + + for child_id in (1, 2, 3, 4): + entity_id = f"{platform}.child_{child_id}_{translated_name}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + for device_id in ("child1", "child2", "child3", "child4"): + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + assert device_entry + assert device_entry.via_device_id == parent_device.id + + +@pytest.mark.parametrize( + ("platform", "modules", "features", "translated_name", "child_device_type"), + [ + pytest.param( + "camera", [Module.Camera], [], "live_view", DeviceType.Camera, id="camera" + ), + pytest.param("fan", [Module.Fan], [], None, DeviceType.Fan, id="fan"), + pytest.param("siren", [Module.Alarm], [], None, DeviceType.Camera, id="siren"), + pytest.param("light", [Module.Light], [], None, DeviceType.Camera, id="light"), + pytest.param( + "light", + [Module.Light, Module.LightEffect], + [], + None, + DeviceType.Camera, + id="light_effect", + ), + pytest.param( + "climate", + [Module.Thermostat], + ["temperature", "target_temperature"], + None, + DeviceType.Thermostat, + id="climate", + ), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_automatic_module_device_addition_and_removal( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + mock_connect: AsyncMock, + mock_discovery: AsyncMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, + platform: str, + modules: list[str], + features: list[str], + translated_name: str | None, + child_device_type: DeviceType, +) -> None: + """Test for automatic device with modules addition and removal.""" + + children = { + f"child{index}": _mocked_device( + alias=f"child {index}", + modules=modules, + features=features, + device_type=child_device_type, + device_id=f"child{index}", + ) + for index in range(1, 5) + } + + mock_device = _mocked_device( + alias="hub", + children=[children["child1"], children["child2"]], + features=["ssid"], + device_type=DeviceType.Hub, + device_id="hub_parent", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + # Set the parent property for the dynamic children as mock_device only sets + # it on initialization + for child in children.values(): + child.parent = mock_device + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + mock_camera_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_camera_config_entry.entry_id) + await hass.async_block_till_done() + + for child_id in (1, 2): + sub_id = f"_{translated_name}" if translated_name else "" + entity_id = f"{platform}.child_{child_id}{sub_id}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + parent_device = device_registry.async_get_device( + identifiers={(DOMAIN, "hub_parent")} + ) + assert parent_device + + for device_id in ("child1", "child2"): + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + assert device_entry + assert device_entry.via_device_id == parent_device.id + + # Remove one of the devices + mock_device.children = [children["child1"]] + freezer.tick(5) + async_fire_time_changed(hass) + + sub_id = f"_{translated_name}" if translated_name else "" + entity_id = f"{platform}.child_2{sub_id}" + state = hass.states.get(entity_id) + assert state is None + assert entity_registry.async_get(entity_id) is None + + assert device_registry.async_get_device(identifiers={(DOMAIN, "child2")}) is None + + # Re-dd the previously removed child device + mock_device.children = [ + children["child1"], + children["child2"], + ] + freezer.tick(5) + async_fire_time_changed(hass) + + for child_id in (1, 2): + sub_id = f"_{translated_name}" if translated_name else "" + entity_id = f"{platform}.child_{child_id}{sub_id}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + for device_id in ("child1", "child2"): + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + assert device_entry + assert device_entry.via_device_id == parent_device.id + + # Add child devices + mock_device.children = [children["child1"], children["child3"], children["child4"]] + freezer.tick(5) + async_fire_time_changed(hass) + + for child_id in (1, 3, 4): + sub_id = f"_{translated_name}" if translated_name else "" + entity_id = f"{platform}.child_{child_id}{sub_id}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + for device_id in ("child1", "child3", "child4"): + assert device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + + # Add the previously removed child device + mock_device.children = [ + children["child1"], + children["child2"], + children["child3"], + children["child4"], + ] + freezer.tick(5) + async_fire_time_changed(hass) + + for child_id in (1, 2, 3, 4): + sub_id = f"_{translated_name}" if translated_name else "" + entity_id = f"{platform}.child_{child_id}{sub_id}" + state = hass.states.get(entity_id) + assert state + assert entity_registry.async_get(entity_id) + + for device_id in ("child1", "child2", "child3", "child4"): + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + assert device_entry + assert device_entry.via_device_id == parent_device.id + + +async def test_automatic_device_addition_does_not_remove_disabled_default( + hass: HomeAssistant, + mock_camera_config_entry: MockConfigEntry, + mock_connect: AsyncMock, + mock_discovery: AsyncMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test for automatic device addition does not remove disabled default entities.""" + + features = ["ssid", "signal_level"] + children = { + f"child{index}": _mocked_device( + alias=f"child {index}", + features=features, + device_id=f"child{index}", + ) + for index in range(1, 5) + } + + mock_device = _mocked_device( + alias="hub", + children=[children["child1"], children["child2"]], + features=features, + device_type=DeviceType.Hub, + device_id="hub_parent", + ip_address=IP_ADDRESS3, + mac=MAC_ADDRESS3, + ) + # Set the parent property for the dynamic children as mock_device only sets + # it on initialization + for child in children.values(): + child.parent = mock_device + + with override_side_effect(mock_connect["connect"], lambda *_, **__: mock_device): + mock_camera_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_camera_config_entry.entry_id) + await hass.async_block_till_done() + + def check_entities(entity_id_device): + entity_id = f"sensor.{entity_id_device}_signal_level" + state = hass.states.get(entity_id) + assert state + reg_ent = entity_registry.async_get(entity_id) + assert reg_ent + assert reg_ent.disabled is False + + entity_id = f"sensor.{entity_id_device}_ssid" + state = hass.states.get(entity_id) + assert state is None + reg_ent = entity_registry.async_get(entity_id) + assert reg_ent + assert reg_ent.disabled is True + assert reg_ent.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + check_entities("hub") + for child_id in (1, 2): + check_entities(f"child_{child_id}") + + # Add child devices + mock_device.children = [children["child1"], children["child2"], children["child3"]] + freezer.tick(5) + async_fire_time_changed(hass) + + check_entities("hub") + for child_id in (1, 2, 3): + check_entities(f"child_{child_id}") diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index b7f4ed6b8f4..7ae21fb4a26 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta +import re from unittest.mock import MagicMock, PropertyMock from freezegun.api import FrozenDateTimeFactory @@ -19,6 +20,11 @@ from kasa.iot import IotDevice import pytest from homeassistant.components import tplink +from homeassistant.components.homeassistant.scene import ( + CONF_SCENE_ID, + CONF_SNAPSHOT, + SERVICE_CREATE, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -34,8 +40,15 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, EFFECT_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.components.tplink.const import DOMAIN +from homeassistant.components.tplink.light import ( + SERVICE_RANDOM_EFFECT, + SERVICE_SEQUENCE_EFFECT, +) from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, @@ -48,16 +61,16 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, + _mocked_feature, _patch_connect, _patch_discovery, _patch_single_discovery, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry, async_fire_time_changed @@ -82,7 +95,7 @@ async def test_light_unique_id( light = _mocked_device(modules=[Module.Light], alias="my_light") light.device_type = device_type with _patch_discovery(device=light), _patch_connect(device=light): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -92,8 +105,11 @@ async def test_light_unique_id( ) -async def test_legacy_dimmer_unique_id(hass: HomeAssistant) -> None: - """Test a light unique id.""" +async def test_legacy_dimmer_unique_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test dimmer unique id.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) @@ -107,23 +123,52 @@ async def test_legacy_dimmer_unique_id(hass: HomeAssistant) -> None: light.device_type = DeviceType.Dimmer with _patch_discovery(device=light), _patch_connect(device=light): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" - entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == "aa:bb:cc:dd:ee:ff" @pytest.mark.parametrize( - ("device", "transition"), + ("device", "extra_data", "expected_transition"), [ - (_mocked_device(modules=[Module.Light]), 2.0), - (_mocked_device(modules=[Module.Light, Module.LightEffect]), None), + ( + _mocked_device( + modules=[Module.Light], + features=[ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ], + ), + {ATTR_TRANSITION: 2.0}, + 2.0 * 1_000, + ), + ( + _mocked_device( + modules=[Module.Light], + features=[ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ], + ), + {}, + None, + ), ], ) async def test_color_light( - hass: HomeAssistant, device: MagicMock, transition: float | None + hass: HomeAssistant, + device: MagicMock, + extra_data: dict, + expected_transition: float | None, ) -> None: """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( @@ -131,101 +176,230 @@ async def test_color_light( ) already_migrated_config_entry.add_to_hass(hass) light = device.modules[Module.Light] + + # Setting color_temp to None emulates a device without color temp light.color_temp = None + with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_bulb" - KASA_TRANSITION_VALUE = transition * 1_000 if transition is not None else None BASE_PAYLOAD = {ATTR_ENTITY_ID: entity_id} - if transition: - BASE_PAYLOAD[ATTR_TRANSITION] = transition + BASE_PAYLOAD |= extra_data state = hass.states.get(entity_id) assert state.state == "on" attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] - # If effect is active, only the brightness can be controlled - if attributes.get(ATTR_EFFECT) is not None: - assert attributes[ATTR_COLOR_MODE] == "brightness" - else: - assert attributes[ATTR_COLOR_MODE] == "hs" - assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 4000 - assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 9000 - assert attributes[ATTR_HS_COLOR] == (10, 30) - assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) - assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) + + assert attributes.get(ATTR_EFFECT) is None + + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 4000 + assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 9000 + assert attributes[ATTR_HS_COLOR] == (10, 30) + assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) + assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, BASE_PAYLOAD, blocking=True ) light.set_state.assert_called_once_with( - LightState(light_on=False, transition=KASA_TRANSITION_VALUE) + LightState(light_on=False, transition=expected_transition) ) light.set_state.reset_mock() - await hass.services.async_call(LIGHT_DOMAIN, "turn_on", BASE_PAYLOAD, blocking=True) + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, BASE_PAYLOAD, blocking=True + ) light.set_state.assert_called_once_with( - LightState(light_on=True, transition=KASA_TRANSITION_VALUE) + LightState(light_on=True, transition=expected_transition) ) light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100}, blocking=True, ) - light.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE) + light.set_brightness.assert_called_with(39, transition=expected_transition) light.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) light.set_color_temp.assert_called_with( - 6666, brightness=None, transition=KASA_TRANSITION_VALUE + 6666, brightness=None, transition=expected_transition ) light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) light.set_color_temp.assert_called_with( - 6666, brightness=None, transition=KASA_TRANSITION_VALUE + 6666, brightness=None, transition=expected_transition ) light.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - light.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) + light.set_hsv.assert_called_with(10, 30, None, transition=expected_transition) light.set_hsv.reset_mock() -async def test_color_light_no_temp(hass: HomeAssistant) -> None: - """Test a light.""" +@pytest.mark.parametrize( + ("device", "extra_data", "expected_transition"), + [ + ( + _mocked_device( + modules=[Module.Light, Module.LightEffect], + features=[ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ], + ), + {ATTR_TRANSITION: 2.0}, + 2.0 * 1_000, + ), + ( + _mocked_device( + modules=[Module.Light, Module.LightEffect], + features=[ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ], + ), + {}, + None, + ), + ], +) +async def test_color_light_with_active_effect( + hass: HomeAssistant, + device: MagicMock, + extra_data: dict, + expected_transition: float | None, +) -> None: + """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - device = _mocked_device(modules=[Module.Light], alias="my_light") light = device.modules[Module.Light] - light.is_variable_color_temp = False + + with _patch_discovery(device=device), _patch_connect(device=device): + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + BASE_PAYLOAD = {ATTR_ENTITY_ID: entity_id} + BASE_PAYLOAD |= extra_data + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] + + # If effect is active, only the brightness can be controlled + assert attributes.get(ATTR_EFFECT) is not None + assert attributes[ATTR_COLOR_MODE] == "brightness" + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, BASE_PAYLOAD, blocking=True + ) + light.set_state.assert_called_once_with( + LightState(light_on=False, transition=expected_transition) + ) + light.set_state.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, BASE_PAYLOAD, blocking=True + ) + light.set_state.assert_called_once_with( + LightState(light_on=True, transition=expected_transition) + ) + light.set_state.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + light.set_brightness.assert_called_with(39, transition=expected_transition) + light.set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, + blocking=True, + ) + light.set_color_temp.assert_called_with( + 6666, brightness=None, transition=expected_transition + ) + light.set_color_temp.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {**BASE_PAYLOAD, ATTR_COLOR_TEMP_KELVIN: 6666}, + blocking=True, + ) + light.set_color_temp.assert_called_with( + 6666, brightness=None, transition=expected_transition + ) + light.set_color_temp.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + light.set_hsv.assert_called_with(10, 30, None, transition=expected_transition) + light.set_hsv.reset_mock() + + +async def test_color_light_no_temp(hass: HomeAssistant) -> None: + """Test a color light with no color temp.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + features = [ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + ] + + device = _mocked_device(modules=[Module.Light], alias="my_light", features=features) + light = device.modules[Module.Light] + type(light).color_temp = PropertyMock(side_effect=Exception) with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -241,20 +415,20 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) @@ -263,7 +437,7 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) @@ -271,29 +445,28 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: light.set_hsv.reset_mock() -@pytest.mark.parametrize( - ("bulb", "is_color"), - [ - (_mocked_device(modules=[Module.Light], alias="my_light"), True), - (_mocked_device(modules=[Module.Light], alias="my_light"), False), - ], -) -async def test_color_temp_light( - hass: HomeAssistant, bulb: MagicMock, is_color: bool -) -> None: - """Test a light.""" +async def test_color_temp_light_color(hass: HomeAssistant) -> None: + """Test a color temp light with color.""" + device = _mocked_device( + modules=[Module.Light], + alias="my_light", + features=[ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ], + ) already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - device = _mocked_device(modules=[Module.Light], alias="my_light") + light = device.modules[Module.Light] - light.is_color = is_color - light.color_temp = 4000 - light.is_variable_color_temp = True with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -303,29 +476,24 @@ async def test_color_temp_light( attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "color_temp" - if light.is_color: - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] - else: - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] - assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 9000 - assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 4000 - assert attributes[ATTR_COLOR_TEMP_KELVIN] == 4000 + + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) @@ -334,7 +502,7 @@ async def test_color_temp_light( await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 6666}, blocking=True, ) @@ -344,7 +512,7 @@ async def test_color_temp_light( # Verify color temp is clamped to the valid range await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000}, blocking=True, ) @@ -354,7 +522,94 @@ async def test_color_temp_light( # Verify color temp is clamped to the valid range await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1}, + blocking=True, + ) + light.set_color_temp.assert_called_with(4000, brightness=None, transition=None) + light.set_color_temp.reset_mock() + + +async def test_color_temp_light_no_color(hass: HomeAssistant) -> None: + """Test a color temp light with no color.""" + device = _mocked_device( + modules=[Module.Light], + alias="my_light", + features=[ + _mocked_feature("brightness", value=50), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ], + ) + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + + light = device.modules[Module.Light] + + with _patch_discovery(device=device), _patch_connect(device=device): + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = "light.my_light" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "color_temp" + + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + assert attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 9000 + assert attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 4000 + assert attributes[ATTR_COLOR_TEMP_KELVIN] == 4000 + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + light.set_state.assert_called_once() + light.set_state.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + light.set_state.assert_called_once() + light.set_state.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + light.set_brightness.assert_called_with(39, transition=None) + light.set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 6666}, + blocking=True, + ) + light.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + light.set_color_temp.reset_mock() + + # Verify color temp is clamped to the valid range + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000}, + blocking=True, + ) + light.set_color_temp.assert_called_with(9000, brightness=None, transition=None) + light.set_color_temp.reset_mock() + + # Verify color temp is clamped to the valid range + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1}, blocking=True, ) @@ -363,18 +618,19 @@ async def test_color_temp_light( async def test_brightness_only_light(hass: HomeAssistant) -> None: - """Test a light.""" + """Test a light brightness.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - device = _mocked_device(modules=[Module.Light], alias="my_light") + features = [ + _mocked_feature("brightness", value=50), + ] + device = _mocked_device(modules=[Module.Light], alias="my_light", features=features) light = device.modules[Module.Light] - light.is_color = False - light.is_variable_color_temp = False with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -387,20 +643,20 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) @@ -409,19 +665,16 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: async def test_on_off_light(hass: HomeAssistant) -> None: - """Test a light.""" + """Test a light turns on and off.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - device = _mocked_device(modules=[Module.Light], alias="my_light") + device = _mocked_device(modules=[Module.Light], alias="my_light", features=[]) light = device.modules[Module.Light] - light.is_color = False - light.is_variable_color_temp = False - light.is_dimmable = False with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -432,33 +685,31 @@ async def test_on_off_light(hass: HomeAssistant) -> None: assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once() light.set_state.reset_mock() async def test_off_at_start_light(hass: HomeAssistant) -> None: - """Test a light.""" + """Test a light off at startup.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - device = _mocked_device(modules=[Module.Light], alias="my_light") + device = _mocked_device(modules=[Module.Light], alias="my_light", features=[]) light = device.modules[Module.Light] - light.is_color = False - light.is_variable_color_temp = False - light.is_dimmable = False + light.state = LightState(light_on=False) with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -470,7 +721,7 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None: async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: - """Test a light.""" + """Test a dimmer turns on without brightness being set.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) @@ -481,7 +732,7 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: light.state = LightState(light_on=False) with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -490,7 +741,7 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: assert state.state == "off" await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) light.set_state.assert_called_once_with( LightState( @@ -513,8 +764,15 @@ async def test_smart_strip_effects( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) + features = [ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ] device = _mocked_device( - modules=[Module.Light, Module.LightEffect], alias="my_light" + modules=[Module.Light, Module.LightEffect], alias="my_light", features=features ) light = device.modules[Module.Light] light_effect = device.modules[Module.LightEffect] @@ -524,7 +782,7 @@ async def test_smart_strip_effects( _patch_single_discovery(device=device), _patch_connect(device=device), ): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -538,7 +796,7 @@ async def test_smart_strip_effects( # is in progress calls set_effect to clear the effect await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 4000}, blocking=True, ) @@ -549,7 +807,7 @@ async def test_smart_strip_effects( await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect2"}, blocking=True, ) @@ -566,7 +824,7 @@ async def test_smart_strip_effects( # Test setting light effect off await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "off"}, blocking=True, ) @@ -581,7 +839,7 @@ async def test_smart_strip_effects( caplog.clear() await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect3"}, blocking=True, ) @@ -610,7 +868,7 @@ async def test_smart_strip_effects( await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) @@ -640,7 +898,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: light_effect = device.modules[Module.LightEffect] with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -650,7 +908,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, - "random_effect", + SERVICE_RANDOM_EFFECT, { ATTR_ENTITY_ID: entity_id, "init_states": [340, 20, 50], @@ -679,7 +937,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, - "random_effect", + SERVICE_RANDOM_EFFECT, { ATTR_ENTITY_ID: entity_id, "init_states": [340, 20, 50], @@ -729,7 +987,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) @@ -738,7 +996,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, - "random_effect", + SERVICE_RANDOM_EFFECT, { ATTR_ENTITY_ID: entity_id, "init_states": [340, 20, 50], @@ -781,6 +1039,84 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: light_effect.set_custom_effect.reset_mock() +@pytest.mark.parametrize( + ("service_name", "service_params", "expected_extra_params"), + [ + pytest.param( + SERVICE_SEQUENCE_EFFECT, + { + "sequence": [[340, 20, 50], [20, 50, 50], [0, 100, 50]], + }, + { + "type": "sequence", + "sequence": [(340, 20, 50), (20, 50, 50), (0, 100, 50)], + "repeat_times": 0, + "spread": 1, + "direction": 4, + }, + id="sequence", + ), + pytest.param( + SERVICE_RANDOM_EFFECT, + {"init_states": [340, 20, 50]}, + {"type": "random", "init_states": [[340, 20, 50]], "random_seed": 100}, + id="random", + ), + ], +) +async def test_smart_strip_effect_service_error( + hass: HomeAssistant, + service_name: str, + service_params: dict, + expected_extra_params: dict, +) -> None: + """Test smart strip effect service errors.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + device = _mocked_device( + modules=[Module.Light, Module.LightEffect], alias="my_light" + ) + light_effect = device.modules[Module.LightEffect] + + with _patch_discovery(device=device), _patch_connect(device=device): + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = "light.my_light" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + light_effect.set_custom_effect.side_effect = KasaException("failed") + + base = { + "custom": 1, + "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", + "brightness": 100, + "name": "Custom", + "segments": [0], + "expansion_strategy": 1, + "enable": 1, + "duration": 0, + "transition": 0, + } + expected_params = {**base, **expected_extra_params} + expected_msg = f"Error trying to set custom effect {expected_params}: failed" + + with pytest.raises(HomeAssistantError, match=re.escape(expected_msg)): + await hass.services.async_call( + DOMAIN, + service_name, + { + ATTR_ENTITY_ID: entity_id, + **service_params, + }, + blocking=True, + ) + + async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> None: """Test smart strip custom random effects at startup.""" already_migrated_config_entry = MockConfigEntry( @@ -794,7 +1130,7 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> light_effect = device.modules[Module.LightEffect] light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -804,7 +1140,7 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> # fallback to set HSV when custom effect is not known so it does turn back on await hass.services.async_call( LIGHT_DOMAIN, - "turn_on", + SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) @@ -824,7 +1160,7 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: light_effect = device.modules[Module.LightEffect] with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -834,7 +1170,7 @@ async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: await hass.services.async_call( DOMAIN, - "sequence_effect", + SERVICE_SEQUENCE_EFFECT, { ATTR_ENTITY_ID: entity_id, "sequence": [[340, 20, 50], [20, 50, 50], [0, 100, 50]], @@ -899,7 +1235,7 @@ async def test_light_errors_when_turned_on( light.set_state.side_effect = exception_type(msg) with _patch_discovery(device=device), _patch_connect(device=device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_light" @@ -910,7 +1246,7 @@ async def test_light_errors_when_turned_on( with pytest.raises(HomeAssistantError, match=msg): await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() assert light.set_state.call_count == 1 @@ -950,7 +1286,7 @@ async def test_light_child( ) with _patch_discovery(device=parent_device), _patch_connect(device=parent_device): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "light.my_device" @@ -977,21 +1313,30 @@ async def test_scene_effect_light( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) + features = [ + _mocked_feature("brightness", value=50), + _mocked_feature("hsv", value=(10, 30, 5)), + _mocked_feature( + "color_temp", value=4000, minimum_value=4000, maximum_value=9000 + ), + ] device = _mocked_device( - modules=[Module.Light, Module.LightEffect], alias="my_light" + modules=[Module.Light, Module.LightEffect], alias="my_light", features=features ) light_effect = device.modules[Module.LightEffect] light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF with _patch_discovery(device=device), _patch_connect(device=device): - assert await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) - assert await async_setup_component(hass, "scene", {}) + assert await hass.config_entries.async_setup( + already_migrated_config_entry.entry_id + ) + assert await async_setup_component(hass, SCENE_DOMAIN, {}) await hass.async_block_till_done() entity_id = "light.my_light" await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() freezer.tick(5) @@ -1003,9 +1348,9 @@ async def test_scene_effect_light( assert state.attributes["effect"] is EFFECT_OFF await hass.services.async_call( - "scene", - "create", - {"scene_id": "effect_off_scene", "snapshot_entities": [entity_id]}, + SCENE_DOMAIN, + SERVICE_CREATE, + {CONF_SCENE_ID: "effect_off_scene", CONF_SNAPSHOT: [entity_id]}, blocking=True, ) await hass.async_block_till_done() @@ -1013,7 +1358,7 @@ async def test_scene_effect_light( assert scene_state.state is STATE_UNKNOWN await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() freezer.tick(5) @@ -1024,10 +1369,10 @@ async def test_scene_effect_light( assert state.state is STATE_OFF await hass.services.async_call( - "scene", - "turn_on", + SCENE_DOMAIN, + SERVICE_TURN_ON, { - "entity_id": "scene.effect_off_scene", + ATTR_ENTITY_ID: "scene.effect_off_scene", }, blocking=True, ) diff --git a/tests/components/tplink/test_number.py b/tests/components/tplink/test_number.py index 865ce27ffc0..07d64178dfa 100644 --- a/tests/components/tplink/test_number.py +++ b/tests/components/tplink/test_number.py @@ -3,7 +3,6 @@ from kasa import Feature from syrupy.assertion import SnapshotAssertion -from homeassistant.components import tplink from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, @@ -15,11 +14,8 @@ from homeassistant.components.tplink.number import NUMBER_DESCRIPTIONS from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, _mocked_feature, _mocked_strip_children, @@ -28,6 +24,7 @@ from . import ( setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry @@ -39,7 +36,7 @@ async def test_states( device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test a sensor unique ids.""" + """Test a number states.""" features = {description.key for description in NUMBER_DESCRIPTIONS} features.update(EXCLUDED_FEATURES) device = _mocked_device(alias="my_device", features=features) @@ -54,7 +51,7 @@ async def test_states( async def test_number(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: - """Test a sensor unique ids.""" + """Test number unique ids.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) @@ -70,7 +67,7 @@ async def test_number(hass: HomeAssistant, entity_registry: er.EntityRegistry) - ) plug = _mocked_device(alias="my_plug", features=[new_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "number.my_plug_temperature_offset" @@ -84,7 +81,7 @@ async def test_number_children( entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: - """Test a sensor unique ids.""" + """Test number children.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) @@ -104,7 +101,7 @@ async def test_number_children( children=_mocked_strip_children(features=[new_feature]), ) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "number.my_plug_temperature_offset" @@ -142,7 +139,7 @@ async def test_number_set( ) plug = _mocked_device(alias="my_plug", features=[new_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "number.my_plug_temperature_offset" diff --git a/tests/components/tplink/test_select.py b/tests/components/tplink/test_select.py index 6c49185d91c..3b99412740a 100644 --- a/tests/components/tplink/test_select.py +++ b/tests/components/tplink/test_select.py @@ -4,7 +4,6 @@ from kasa import Feature import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import tplink from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, @@ -16,11 +15,8 @@ from homeassistant.components.tplink.select import SELECT_DESCRIPTIONS from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, _mocked_feature, _mocked_strip_children, @@ -29,13 +25,14 @@ from . import ( setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry @pytest.fixture def mocked_feature_select() -> Feature: - """Return mocked tplink binary sensor feature.""" + """Return mocked tplink select feature.""" return _mocked_feature( "light_preset", value="First choice", @@ -53,7 +50,7 @@ async def test_states( device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test a sensor unique ids.""" + """Test select states.""" features = {description.key for description in SELECT_DESCRIPTIONS} features.update(EXCLUDED_FEATURES) device = _mocked_device(alias="my_device", features=features) @@ -72,7 +69,7 @@ async def test_select( entity_registry: er.EntityRegistry, mocked_feature_select: Feature, ) -> None: - """Test a sensor unique ids.""" + """Test select unique ids.""" mocked_feature = mocked_feature_select already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS @@ -81,7 +78,7 @@ async def test_select( plug = _mocked_device(alias="my_plug", features=[mocked_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() # The entity_id is based on standard name from core. @@ -97,7 +94,7 @@ async def test_select_children( device_registry: dr.DeviceRegistry, mocked_feature_select: Feature, ) -> None: - """Test a sensor unique ids.""" + """Test select children.""" mocked_feature = mocked_feature_select already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS @@ -109,7 +106,7 @@ async def test_select_children( children=_mocked_strip_children(features=[mocked_feature]), ) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "select.my_plug_light_preset" @@ -141,7 +138,7 @@ async def test_select_select( already_migrated_config_entry.add_to_hass(hass) plug = _mocked_device(alias="my_plug", features=[mocked_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "select.my_plug_light_preset" diff --git a/tests/components/tplink/test_sensor.py b/tests/components/tplink/test_sensor.py index dda43c52430..857a2365527 100644 --- a/tests/components/tplink/test_sensor.py +++ b/tests/components/tplink/test_sensor.py @@ -4,18 +4,14 @@ from kasa import Device, Feature, Module import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN from homeassistant.components.tplink.entity import EXCLUDED_FEATURES from homeassistant.components.tplink.sensor import SENSOR_DESCRIPTIONS from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, _mocked_energy_features, _mocked_feature, @@ -25,6 +21,7 @@ from . import ( setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry @@ -36,7 +33,7 @@ async def test_states( device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test a sensor unique ids.""" + """Test a sensor states.""" features = {description.key for description in SENSOR_DESCRIPTIONS} features.update(EXCLUDED_FEATURES) device = _mocked_device(alias="my_device", features=features) @@ -67,7 +64,7 @@ async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None: alias="my_bulb", modules=[Module.Light], features=["state", *emeter_features] ) with _patch_discovery(device=bulb), _patch_connect(device=bulb): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() await hass.async_block_till_done() @@ -104,7 +101,7 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: ) plug = _mocked_device(alias="my_plug", features=["state", *emeter_features]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() await hass.async_block_till_done() @@ -129,9 +126,9 @@ async def test_color_light_no_emeter(hass: HomeAssistant) -> None: ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_device(alias="my_bulb", modules=[Module.Light]) - bulb.has_emeter = False + with _patch_discovery(device=bulb), _patch_connect(device=bulb): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() await hass.async_block_till_done() @@ -167,7 +164,7 @@ async def test_sensor_unique_id( ) plug = _mocked_device(alias="my_plug", features=emeter_features) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() expected = { @@ -202,7 +199,7 @@ async def test_undefined_sensor( ) plug = _mocked_device(alias="my_plug", features=[new_feature]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() msg = ( @@ -240,7 +237,7 @@ async def test_sensor_children_on_parent( device_type=Device.Type.WallSwitch, ) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "sensor.my_plug_this_month_s_consumption" @@ -288,7 +285,7 @@ async def test_sensor_children_on_child( device_type=Device.Type.Strip, ) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "sensor.my_plug_this_month_s_consumption" @@ -308,19 +305,18 @@ async def test_sensor_children_on_child( assert child_device.via_device_id == device.id -@pytest.mark.skip -async def test_new_datetime_sensor( +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_datetime_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: - """Test a sensor unique ids.""" - # Skipped temporarily while datetime handling on hold. + """Test a timestamp sensor.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) plug = _mocked_device(alias="my_plug", features=["on_since"]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "sensor.my_plug_on_since" diff --git a/tests/components/tplink/test_siren.py b/tests/components/tplink/test_siren.py index 8c3328558b0..1d820bca1d1 100644 --- a/tests/components/tplink/test_siren.py +++ b/tests/components/tplink/test_siren.py @@ -7,12 +7,16 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.siren import ( + ATTR_DURATION, + ATTR_TONE, + ATTR_VOLUME_LEVEL, DOMAIN as SIREN_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr, entity_registry as er from . import _mocked_device, setup_platform_for_device, snapshot_platform @@ -74,3 +78,91 @@ async def test_turn_on_and_off( ) alarm_module.play.assert_called() + + +@pytest.mark.parametrize( + ("max_volume", "volume_level", "expected_volume"), + [ + pytest.param(3, 0.1, 1, id="smart-10%"), + pytest.param(3, 0.3, 1, id="smart-30%"), + pytest.param(3, 0.99, 3, id="smart-99%"), + pytest.param(3, 1, 3, id="smart-100%"), + pytest.param(10, 0.1, 1, id="smartcam-10%"), + pytest.param(10, 0.3, 3, id="smartcam-30%"), + pytest.param(10, 0.99, 10, id="smartcam-99%"), + pytest.param(10, 1, 10, id="smartcam-100%"), + ], +) +async def test_turn_on_with_volume( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_hub: Device, + max_volume: int, + volume_level: float, + expected_volume: int, +) -> None: + """Test that turn_on volume parameters work as expected.""" + + alarm_module = mocked_hub.modules[Module.Alarm] + alarm_volume_feat = alarm_module.get_feature("alarm_volume") + assert alarm_volume_feat + alarm_volume_feat.maximum_value = max_volume + + await setup_platform_for_device(hass, mock_config_entry, Platform.SIREN, mocked_hub) + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_VOLUME_LEVEL: volume_level}, + blocking=True, + ) + + alarm_module.play.assert_called_with( + volume=expected_volume, duration=None, sound=None + ) + + +async def test_turn_on_with_duration_and_sound( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_hub: Device, +) -> None: + """Test that turn_on tone and duration parameters work as expected.""" + + alarm_module = mocked_hub.modules[Module.Alarm] + + await setup_platform_for_device(hass, mock_config_entry, Platform.SIREN, mocked_hub) + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [ENTITY_ID], ATTR_DURATION: 5, ATTR_TONE: "Foo"}, + blocking=True, + ) + + alarm_module.play.assert_called_with(volume=None, duration=5, sound="Foo") + + +@pytest.mark.parametrize(("duration"), [0, 301]) +async def test_turn_on_with_invalid_duration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_hub: Device, + duration: int, +) -> None: + """Test that turn_on with invalid_duration raises an error.""" + + await setup_platform_for_device(hass, mock_config_entry, Platform.SIREN, mocked_hub) + + msg = f"Invalid duration {duration} available: 1-300s" + + with pytest.raises(ServiceValidationError, match=msg): + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: [ENTITY_ID], + ATTR_DURATION: duration, + }, + blocking=True, + ) diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index e9c8cc07b67..bdf54f10e8b 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -9,7 +9,11 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import tplink -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.components.tplink.const import DOMAIN from homeassistant.components.tplink.entity import EXCLUDED_FEATURES from homeassistant.components.tplink.switch import SWITCH_DESCRIPTIONS @@ -25,12 +29,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify from . import ( - DEVICE_ID, - MAC_ADDRESS, _mocked_device, _mocked_strip_children, _patch_connect, @@ -38,6 +39,7 @@ from . import ( setup_platform_for_device, snapshot_platform, ) +from .const import DEVICE_ID, MAC_ADDRESS from tests.common import MockConfigEntry, async_fire_time_changed @@ -49,7 +51,7 @@ async def test_states( device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: - """Test a sensor unique ids.""" + """Test a switch states.""" features = {description.key for description in SWITCH_DESCRIPTIONS} features.update(EXCLUDED_FEATURES) device = _mocked_device(alias="my_device", features=features) @@ -72,7 +74,7 @@ async def test_plug(hass: HomeAssistant) -> None: plug = _mocked_device(alias="my_plug", features=["state"]) feat = plug.features["state"] with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_plug" @@ -80,13 +82,13 @@ async def test_plug(hass: HomeAssistant) -> None: assert state.state == STATE_ON await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) feat.set_value.assert_called_once() feat.set_value.reset_mock() await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) feat.set_value.assert_called_once() feat.set_value.reset_mock() @@ -120,7 +122,7 @@ async def test_led_switch(hass: HomeAssistant, dev: Device, domain: str) -> None feat = dev.features["led"] already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(device=dev), _patch_connect(device=dev): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_name = slugify(dev.alias) @@ -131,13 +133,13 @@ async def test_led_switch(hass: HomeAssistant, dev: Device, domain: str) -> None assert led_state.name == f"{dev.alias} LED" await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: led_entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) feat.set_value.assert_called_once_with(False) feat.set_value.reset_mock() await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: led_entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: led_entity_id}, blocking=True ) feat.set_value.assert_called_once_with(True) feat.set_value.reset_mock() @@ -153,7 +155,7 @@ async def test_plug_unique_id( already_migrated_config_entry.add_to_hass(hass) plug = _mocked_device(alias="my_plug", features=["state", "led"]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_plug" @@ -168,7 +170,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None: already_migrated_config_entry.add_to_hass(hass) plug = _mocked_device(alias="my_plug", features=["state", "led"]) with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_plug" @@ -197,7 +199,7 @@ async def test_strip(hass: HomeAssistant) -> None: strip.children[0].features["state"].value = True strip.children[1].features["state"].value = False with _patch_discovery(device=strip), _patch_connect(device=strip): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_strip_plug0" @@ -205,14 +207,14 @@ async def test_strip(hass: HomeAssistant) -> None: assert state.state == STATE_ON await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) feat = strip.children[0].features["state"] feat.set_value.assert_called_once() feat.set_value.reset_mock() await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) feat.set_value.assert_called_once() feat.set_value.reset_mock() @@ -222,14 +224,14 @@ async def test_strip(hass: HomeAssistant) -> None: assert state.state == STATE_OFF await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) feat = strip.children[1].features["state"] feat.set_value.assert_called_once() feat.set_value.reset_mock() await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) feat.set_value.assert_called_once() feat.set_value.reset_mock() @@ -249,7 +251,7 @@ async def test_strip_unique_ids( features=["state", "led"], ) with _patch_discovery(device=strip), _patch_connect(device=strip): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() for plug_id in range(2): @@ -260,9 +262,11 @@ async def test_strip_unique_ids( async def test_strip_blank_alias( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, ) -> None: - """Test a strip unique id.""" + """Test a strip with blank parent alias.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) @@ -274,14 +278,30 @@ async def test_strip_blank_alias( features=["state", "led"], ) with _patch_discovery(device=strip), _patch_connect(device=strip): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() + strip_entity_id = "switch.unnamed_ks123" + state = hass.states.get(strip_entity_id) + assert state.name == "Unnamed KS123" + reg_ent = entity_registry.async_get(strip_entity_id) + assert reg_ent + reg_dev = device_registry.async_get(reg_ent.device_id) + assert reg_dev + assert reg_dev.name == "Unnamed KS123" + for plug_id in range(2): entity_id = f"switch.unnamed_ks123_stripsocket_{plug_id + 1}" state = hass.states.get(entity_id) assert state.name == f"Unnamed KS123 Stripsocket {plug_id + 1}" + reg_ent = entity_registry.async_get(entity_id) + assert reg_ent + reg_dev = device_registry.async_get(reg_ent.device_id) + assert reg_dev + # Switch is a primary feature so entities go on the parent device. + assert reg_dev.name == "Unnamed KS123" + @pytest.mark.parametrize( ("exception_type", "msg", "reauth_expected"), @@ -320,7 +340,7 @@ async def test_plug_errors_when_turned_on( feat.set_value.side_effect = exception_type("test error") with _patch_discovery(device=plug), _patch_connect(device=plug): - await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.config_entries.async_setup(already_migrated_config_entry.entry_id) await hass.async_block_till_done() entity_id = "switch.my_plug" @@ -331,7 +351,7 @@ async def test_plug_errors_when_turned_on( with pytest.raises(HomeAssistantError, match=msg): await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.async_block_till_done() assert feat.set_value.call_count == 1 diff --git a/tests/components/tplink/test_vacuum.py b/tests/components/tplink/test_vacuum.py new file mode 100644 index 00000000000..55bb8c0b504 --- /dev/null +++ b/tests/components/tplink/test_vacuum.py @@ -0,0 +1,133 @@ +"""Tests for vacuum platform.""" + +from __future__ import annotations + +from kasa import Device, Module +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, + ATTR_FAN_SPEED, + DOMAIN as VACUUM_DOMAIN, + SERVICE_LOCATE, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + VacuumActivity, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import ( + device_registry as dr, + entity_registry as er, + translation, +) + +from . import DEVICE_ID, _mocked_device, setup_platform_for_device, snapshot_platform + +from tests.common import MockConfigEntry + +ENTITY_ID = "vacuum.my_vacuum" + + +@pytest.fixture +async def mocked_vacuum(hass: HomeAssistant) -> Device: + """Return mocked tplink vacuum.""" + + return _mocked_device(modules=[Module.Clean, Module.Speaker], alias="my_vacuum") + + +async def test_vacuum( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + mocked_vacuum: Device, +) -> None: + """Test initialization.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.VACUUM, mocked_vacuum + ) + + device_entries = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + assert device_entries + + entity = entity_registry.async_get(ENTITY_ID) + assert entity + assert entity.unique_id == f"{DEVICE_ID}-vacuum" + + state = hass.states.get(ENTITY_ID) + assert state.state == VacuumActivity.DOCKED + + assert state.attributes[ATTR_FAN_SPEED] == "max" + assert state.attributes[ATTR_BATTERY_LEVEL] == 100 + result = translation.async_translate_state( + hass, "max", "vacuum", "tplink", "vacuum.state_attributes.fan_speed", None + ) + assert result == "Max" + + +async def test_states( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, + mocked_vacuum: Device, +) -> None: + """Test vacuum states.""" + await setup_platform_for_device( + hass, mock_config_entry, Platform.VACUUM, mocked_vacuum + ) + await snapshot_platform( + hass, entity_registry, device_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("service_call", "module_name", "method", "params"), + [ + (SERVICE_START, Module.Clean, "start", {}), + (SERVICE_PAUSE, Module.Clean, "pause", {}), + (SERVICE_RETURN_TO_BASE, Module.Clean, "return_home", {}), + ( + SERVICE_SET_FAN_SPEED, + Module.Clean, + "set_fan_speed_preset", + {ATTR_FAN_SPEED: "quiet"}, + ), + (SERVICE_LOCATE, Module.Speaker, "locate", {}), + ], +) +async def test_vacuum_module( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mocked_vacuum: Device, + service_call: str, + module_name: str, + method: str, + params: dict, +) -> None: + """Test that all vacuum commands work correctly.""" + vacuum = mocked_vacuum + module = vacuum.modules[module_name] + + await setup_platform_for_device(hass, mock_config_entry, Platform.VACUUM, vacuum) + + mock_method = getattr(module, method) + + service_data = {ATTR_ENTITY_ID: ENTITY_ID} + service_data |= params + + await hass.services.async_call( + VACUUM_DOMAIN, service_call, service_data, blocking=True + ) + + # Is this required when using blocking=True? + await hass.async_block_till_done(wait_background_tasks=True) + + mock_method.assert_called() diff --git a/tests/components/tplink_omada/snapshots/test_sensor.ambr b/tests/components/tplink_omada/snapshots/test_sensor.ambr index 6c332eb9696..62167fc9d40 100644 --- a/tests/components/tplink_omada/snapshots/test_sensor.ambr +++ b/tests/components/tplink_omada/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -124,6 +126,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -174,6 +177,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -232,6 +236,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -290,6 +295,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tplink_omada/snapshots/test_switch.ambr b/tests/components/tplink_omada/snapshots/test_switch.ambr index a13d386e721..dde196deaaf 100644 --- a/tests/components/tplink_omada/snapshots/test_switch.ambr +++ b/tests/components/tplink_omada/snapshots/test_switch.ambr @@ -71,6 +71,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -117,6 +118,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tractive/snapshots/test_binary_sensor.ambr b/tests/components/tractive/snapshots/test_binary_sensor.ambr index 4b610e927d5..761626347a7 100644 --- a/tests/components/tractive/snapshots/test_binary_sensor.ambr +++ b/tests/components/tractive/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tractive/snapshots/test_device_tracker.ambr b/tests/components/tractive/snapshots/test_device_tracker.ambr index 4e7c5bfe173..ef511299e68 100644 --- a/tests/components/tractive/snapshots/test_device_tracker.ambr +++ b/tests/components/tractive/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tractive/snapshots/test_sensor.ambr b/tests/components/tractive/snapshots/test_sensor.ambr index f10cfb29226..4551492e36e 100644 --- a/tests/components/tractive/snapshots/test_sensor.ambr +++ b/tests/components/tractive/snapshots/test_sensor.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -116,6 +118,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -164,6 +167,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -213,6 +217,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -263,6 +268,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -313,6 +319,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -367,6 +374,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -419,6 +427,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -475,6 +484,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tractive/snapshots/test_switch.ambr b/tests/components/tractive/snapshots/test_switch.ambr index 08e0c984d0c..d443611ef92 100644 --- a/tests/components/tractive/snapshots/test_switch.ambr +++ b/tests/components/tractive/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index b6f38b1d83d..6d6215a21ab 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -6,10 +6,13 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.tradfri import config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from . import TRADFRI_PATH @@ -115,13 +118,13 @@ async def test_discovery_connection( flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), ip_addresses=[ip_address("123.123.123.123")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + properties={ATTR_PROPERTIES_ID: "homekit-id"}, type="mock_type", ), ) @@ -150,13 +153,13 @@ async def test_discovery_duplicate_aborted(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.124"), ip_addresses=[ip_address("123.123.123.124")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + properties={ATTR_PROPERTIES_ID: "homekit-id"}, type="mock_type", ), ) @@ -174,13 +177,13 @@ async def test_duplicate_discovery( result = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), ip_addresses=[ip_address("123.123.123.123")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + properties={ATTR_PROPERTIES_ID: "homekit-id"}, type="mock_type", ), ) @@ -190,13 +193,13 @@ async def test_duplicate_discovery( result2 = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), ip_addresses=[ip_address("123.123.123.123")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + properties={ATTR_PROPERTIES_ID: "homekit-id"}, type="mock_type", ), ) @@ -215,13 +218,13 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: flow = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), ip_addresses=[ip_address("123.123.123.123")], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "homekit-id"}, + properties={ATTR_PROPERTIES_ID: "homekit-id"}, type="mock_type", ), ) diff --git a/tests/components/trafikverket_train/conftest.py b/tests/components/trafikverket_train/conftest.py index 234269cc9f8..9d7a3873957 100644 --- a/tests/components/trafikverket_train/conftest.py +++ b/tests/components/trafikverket_train/conftest.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest -from pytrafikverket.models import TrainStopModel +from pytrafikverket import StationInfoModel, TrainStopModel from homeassistant.components.trafikverket_train.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -40,6 +40,9 @@ async def load_integration_from_entry( patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", + ), ): await hass.config_entries.async_setup(config_entry_id) await hass.async_block_till_done() @@ -50,8 +53,8 @@ async def load_integration_from_entry( data=ENTRY_CONFIG, options=OPTIONS_CONFIG, entry_id="1", - version=1, - minor_version=2, + version=2, + minor_version=1, ) config_entry.add_to_hass(hass) await setup_config_entry_with_mocked_data(config_entry.entry_id) @@ -61,8 +64,8 @@ async def load_integration_from_entry( source=SOURCE_USER, data=ENTRY_CONFIG2, entry_id="2", - version=1, - minor_version=2, + version=2, + minor_version=1, ) config_entry2.add_to_hass(hass) await setup_config_entry_with_mocked_data(config_entry2.entry_id) @@ -171,3 +174,57 @@ def fixture_get_train_stop() -> TrainStopModel: modified_time=datetime(2023, 5, 1, 11, 0, tzinfo=dt_util.UTC), product_description=["Regionaltåg"], ) + + +@pytest.fixture(name="get_train_stations") +def fixture_get_train_station() -> list[list[StationInfoModel]]: + """Construct StationInfoModel Mock.""" + + return [ + [ + StationInfoModel( + signature="Cst", + station_name="Stockholm C", + advertised=True, + ) + ], + [ + StationInfoModel( + signature="U", + station_name="Uppsala C", + advertised=True, + ) + ], + ] + + +@pytest.fixture(name="get_multiple_train_stations") +def fixture_get_multiple_train_station() -> list[list[StationInfoModel]]: + """Construct StationInfoModel Mock.""" + + return [ + [ + StationInfoModel( + signature="Cst", + station_name="Stockholm C", + advertised=True, + ), + StationInfoModel( + signature="Csu", + station_name="Stockholm City", + advertised=True, + ), + ], + [ + StationInfoModel( + signature="U", + station_name="Uppsala C", + advertised=True, + ), + StationInfoModel( + signature="Ups", + station_name="Uppsala City", + advertised=True, + ), + ], + ] diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index eac5e629bf0..241831b5553 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -5,14 +5,13 @@ from __future__ import annotations from unittest.mock import patch import pytest -from pytrafikverket.exceptions import ( +from pytrafikverket import ( InvalidAuthentication, - MultipleTrainStationsFound, - NoTrainAnnouncementFound, NoTrainStationFound, + StationInfoModel, + TrainStopModel, UnknownError, ) -from pytrafikverket.models import TrainStopModel from homeassistant import config_entries from homeassistant.components.trafikverket_train.const import ( @@ -26,24 +25,27 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import ENTRY_CONFIG, OPTIONS_CONFIG + from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: +async def test_form( + hass: HomeAssistant, get_train_stations: list[StationInfoModel] +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "initial" assert result["errors"] == {} with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", - ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", @@ -67,8 +69,8 @@ async def test_form(hass: HomeAssistant) -> None: assert result["data"] == { "api_key": "1234567890", "name": "Stockholm C to Uppsala C at 10:00", - "from": "Stockholm C", - "to": "Uppsala C", + "from": "Cst", + "to": "U", "time": "10:00", "weekday": ["mon", "fri"], } @@ -76,7 +78,70 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_entry_already_exist(hass: HomeAssistant) -> None: +async def test_form_multiple_stations( + hass: HomeAssistant, get_multiple_train_stations: list[StationInfoModel] +) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_multiple_train_stations, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: ["mon", "fri"], + }, + ) + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_multiple_train_stations, + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_FROM: "Csu", + CONF_TO: "Ups", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Stockholm C to Uppsala C at 10:00" + assert result["data"] == { + "api_key": "1234567890", + "name": "Stockholm C to Uppsala C at 10:00", + "from": "Csu", + "to": "Ups", + "time": "10:00", + "weekday": ["mon", "fri"], + } + assert result["options"] == {"filter_product": None} + + +async def test_form_entry_already_exist( + hass: HomeAssistant, get_train_stations: list[StationInfoModel] +) -> None: """Test flow aborts when entry already exist.""" entry = MockConfigEntry( @@ -84,14 +149,14 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: data={ CONF_API_KEY: "1234567890", CONF_NAME: "Stockholm C to Uppsala C at 10:00", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", + CONF_FROM: "Cst", + CONF_TO: "U", CONF_TIME: "10:00", CONF_WEEKDAY: WEEKDAYS, CONF_FILTER_PRODUCT: None, }, - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) @@ -103,10 +168,11 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", @@ -130,28 +196,24 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - ("side_effect", "base_error"), + ("side_effect", "p_error"), [ ( InvalidAuthentication, - "invalid_auth", + {"base": "invalid_auth"}, ), ( NoTrainStationFound, - "invalid_station", - ), - ( - MultipleTrainStationsFound, - "more_stations", + {"from": "invalid_station", "to": "invalid_station"}, ), ( Exception, - "cannot_connect", + {"base": "cannot_connect"}, ), ], ) async def test_flow_fails( - hass: HomeAssistant, side_effect: Exception, base_error: str + hass: HomeAssistant, side_effect: Exception, p_error: dict[str, str] ) -> None: """Test config flow errors.""" result = await hass.config_entries.flow.async_init( @@ -159,16 +221,13 @@ async def test_flow_fails( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == config_entries.SOURCE_USER + assert result["step_id"] == "initial" with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", side_effect=side_effect(), ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -179,24 +238,24 @@ async def test_flow_fails( }, ) - assert result["errors"] == {"base": base_error} + assert result["errors"] == p_error @pytest.mark.parametrize( - ("side_effect", "base_error"), + ("side_effect", "p_error"), [ ( - NoTrainAnnouncementFound, - "no_trains", + NoTrainStationFound, + {"from": "invalid_station", "to": "invalid_station"}, ), ( UnknownError, - "cannot_connect", + {"base": "cannot_connect"}, ), ], ) async def test_flow_fails_departures( - hass: HomeAssistant, side_effect: Exception, base_error: str + hass: HomeAssistant, side_effect: Exception, p_error: dict[str, str] ) -> None: """Test config flow errors.""" result = await hass.config_entries.flow.async_init( @@ -204,19 +263,13 @@ async def test_flow_fails_departures( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == config_entries.SOURCE_USER + assert result["step_id"] == "initial" with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", - ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_next_train_stops", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", side_effect=side_effect(), ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -227,23 +280,25 @@ async def test_flow_fails_departures( }, ) - assert result["errors"] == {"base": base_error} + assert result["errors"] == p_error -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, get_train_stations: list[StationInfoModel] +) -> None: """Test a reauthentication flow.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_API_KEY: "1234567890", CONF_NAME: "Stockholm C to Uppsala C at 10:00", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", + CONF_FROM: "Cst", + CONF_TO: "U", CONF_TIME: "10:00", CONF_WEEKDAY: WEEKDAYS, }, - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) @@ -254,10 +309,8 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", - ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", @@ -275,8 +328,8 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert entry.data == { "api_key": "1234567891", "name": "Stockholm C to Uppsala C at 10:00", - "from": "Stockholm C", - "to": "Uppsala C", + "from": "Cst", + "to": "U", "time": "10:00", "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], } @@ -287,24 +340,27 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: [ ( InvalidAuthentication, - "invalid_auth", + {"base": "invalid_auth"}, ), ( NoTrainStationFound, - "invalid_station", + {"from": "invalid_station"}, ), ( - MultipleTrainStationsFound, - "more_stations", + UnknownError, + {"base": "cannot_connect"}, ), ( Exception, - "cannot_connect", + {"base": "cannot_connect"}, ), ], ) async def test_reauth_flow_error( - hass: HomeAssistant, side_effect: Exception, p_error: str + hass: HomeAssistant, + side_effect: Exception, + p_error: dict[str, str], + get_train_stations: list[StationInfoModel], ) -> None: """Test a reauthentication flow with error.""" entry = MockConfigEntry( @@ -312,13 +368,13 @@ async def test_reauth_flow_error( data={ CONF_API_KEY: "1234567890", CONF_NAME: "Stockholm C to Uppsala C at 10:00", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", + CONF_FROM: "Cst", + CONF_TO: "U", CONF_TIME: "10:00", CONF_WEEKDAY: WEEKDAYS, }, - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) @@ -326,12 +382,9 @@ async def test_reauth_flow_error( with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", side_effect=side_effect(), ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -341,14 +394,12 @@ async def test_reauth_flow_error( assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": p_error} + assert result["errors"] == p_error with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", - ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.async_setup_entry", @@ -366,8 +417,8 @@ async def test_reauth_flow_error( assert entry.data == { "api_key": "1234567891", "name": "Stockholm C to Uppsala C at 10:00", - "from": "Stockholm C", - "to": "Uppsala C", + "from": "Cst", + "to": "U", "time": "10:00", "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], } @@ -377,17 +428,20 @@ async def test_reauth_flow_error( ("side_effect", "p_error"), [ ( - NoTrainAnnouncementFound, - "no_trains", + NoTrainStationFound, + {"from": "invalid_station"}, ), ( UnknownError, - "cannot_connect", + {"base": "cannot_connect"}, ), ], ) async def test_reauth_flow_error_departures( - hass: HomeAssistant, side_effect: Exception, p_error: str + hass: HomeAssistant, + side_effect: Exception, + p_error: dict[str, str], + get_train_stations: list[StationInfoModel], ) -> None: """Test a reauthentication flow with error.""" entry = MockConfigEntry( @@ -395,13 +449,13 @@ async def test_reauth_flow_error_departures( data={ CONF_API_KEY: "1234567890", CONF_NAME: "Stockholm C to Uppsala C at 10:00", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", + CONF_FROM: "Cst", + CONF_TO: "U", CONF_TIME: "10:00", CONF_WEEKDAY: WEEKDAYS, }, - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) @@ -409,10 +463,7 @@ async def test_reauth_flow_error_departures( with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", - ), - patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", side_effect=side_effect(), ), ): @@ -424,11 +475,12 @@ async def test_reauth_flow_error_departures( assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": p_error} + assert result["errors"] == p_error with ( patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", @@ -449,8 +501,8 @@ async def test_reauth_flow_error_departures( assert entry.data == { "api_key": "1234567891", "name": "Stockholm C to Uppsala C at 10:00", - "from": "Stockholm C", - "to": "Uppsala C", + "from": "Cst", + "to": "U", "time": "10:00", "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], } @@ -460,6 +512,7 @@ async def test_options_flow( hass: HomeAssistant, get_trains: list[TrainStopModel], get_train_stop: TrainStopModel, + get_train_stations: list[StationInfoModel], ) -> None: """Test a reauthentication flow.""" entry = MockConfigEntry( @@ -467,24 +520,28 @@ async def test_options_flow( data={ CONF_API_KEY: "1234567890", CONF_NAME: "Stockholm C to Uppsala C at 10:00", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", + CONF_FROM: "Cst", + CONF_TO: "U", CONF_TIME: "10:00", CONF_WEEKDAY: WEEKDAYS, }, - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) with ( patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", return_value=get_trains, ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", + ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", return_value=get_train_stop, @@ -520,3 +577,328 @@ async def test_options_flow( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {"filter_product": None} + + +async def test_reconfigure_flow( + hass: HomeAssistant, get_train_stations: list[StationInfoModel] +) -> None: + """Test reconfigure flow.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + version=2, + minor_version=1, + ) + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "initial" + assert result["errors"] == {} + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: ["mon", "fri"], + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_multiple_stations( + hass: HomeAssistant, get_multiple_train_stations: list[StationInfoModel] +) -> None: + """Test we can reconfigure with multiple stations.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + version=2, + minor_version=1, + ) + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "initial" + assert result["errors"] == {} + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_multiple_train_stations, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: ["mon", "fri"], + }, + ) + await hass.async_block_till_done() + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_multiple_train_stations, + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_FROM: "Csu", + CONF_TO: "Ups", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_entry_already_exist( + hass: HomeAssistant, get_train_stations: list[StationInfoModel] +) -> None: + """Test flow aborts when entry already exist in a reconfigure flow.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C at 10:00", + CONF_FROM: "Cst", + CONF_TO: "U", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + CONF_FILTER_PRODUCT: None, + }, + version=2, + minor_version=1, + ) + config_entry.add_to_hass(hass) + + config_entry2 = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + version=2, + minor_version=1, + ) + config_entry2.add_to_hass(hass) + result = await config_entry2.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("side_effect", "p_error"), + [ + ( + InvalidAuthentication, + {"base": "invalid_auth"}, + ), + ( + NoTrainStationFound, + {"from": "invalid_station", "to": "invalid_station"}, + ), + ( + Exception, + {"base": "cannot_connect"}, + ), + ], +) +async def test_reconfigure_flow_fails( + hass: HomeAssistant, + side_effect: Exception, + p_error: dict[str, str], + get_train_stations: list[StationInfoModel], +) -> None: + """Test config flow errors.""" + config_entry2 = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + version=2, + minor_version=1, + ) + config_entry2.add_to_hass(hass) + result = await config_entry2.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "initial" + + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", + side_effect=side_effect(), + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + }, + ) + + assert result["errors"] == p_error + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +@pytest.mark.parametrize( + ("side_effect", "p_error"), + [ + ( + NoTrainStationFound, + {"from": "invalid_station", "to": "invalid_station"}, + ), + ( + UnknownError, + {"base": "cannot_connect"}, + ), + ], +) +async def test_reconfigure_flow_fails_departures( + hass: HomeAssistant, + side_effect: Exception, + p_error: dict[str, str], + get_train_stations: list[StationInfoModel], +) -> None: + """Test config flow errors.""" + config_entry2 = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + entry_id="1", + version=2, + minor_version=1, + ) + config_entry2.add_to_hass(hass) + result = await config_entry2.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "initial" + + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", + side_effect=side_effect(), + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + }, + ) + + assert result["errors"] == p_error + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" diff --git a/tests/components/trafikverket_train/test_init.py b/tests/components/trafikverket_train/test_init.py index 41c8e2432ef..cb048365700 100644 --- a/tests/components/trafikverket_train/test_init.py +++ b/tests/components/trafikverket_train/test_init.py @@ -4,8 +4,14 @@ from __future__ import annotations from unittest.mock import patch -from pytrafikverket.exceptions import InvalidAuthentication, NoTrainStationFound -from pytrafikverket.models import TrainStopModel +import pytest +from pytrafikverket import ( + InvalidAuthentication, + NoTrainStationFound, + StationInfoModel, + TrainStopModel, + UnknownError, +) from syrupy.assertion import SnapshotAssertion from homeassistant.components.trafikverket_train.const import DOMAIN @@ -28,14 +34,14 @@ async def test_unload_entry( data=ENTRY_CONFIG, options=OPTIONS_CONFIG, entry_id="1", - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) with ( patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", @@ -65,13 +71,13 @@ async def test_auth_failed( data=ENTRY_CONFIG, options=OPTIONS_CONFIG, entry_id="1", - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) with patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", side_effect=InvalidAuthentication, ): await hass.config_entries.async_setup(entry.entry_id) @@ -96,13 +102,13 @@ async def test_no_stations( data=ENTRY_CONFIG, options=OPTIONS_CONFIG, entry_id="1", - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) with patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", side_effect=NoTrainStationFound, ): await hass.config_entries.async_setup(entry.entry_id) @@ -124,8 +130,8 @@ async def test_migrate_entity_unique_id( data=ENTRY_CONFIG, options=OPTIONS_CONFIG, entry_id="1", - version=1, - minor_version=2, + version=2, + minor_version=1, ) entry.add_to_hass(hass) @@ -139,7 +145,7 @@ async def test_migrate_entity_unique_id( with ( patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", @@ -158,8 +164,9 @@ async def test_migrate_entity_unique_id( async def test_migrate_entry( hass: HomeAssistant, get_trains: list[TrainStopModel], + get_train_stations: list[StationInfoModel], ) -> None: - """Test migrate entry unique id.""" + """Test migrate entry.""" entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, @@ -174,7 +181,11 @@ async def test_migrate_entry( with ( patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_station_from_signature", + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_train_stations, ), patch( "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", @@ -186,8 +197,18 @@ async def test_migrate_entry( assert entry.state is ConfigEntryState.LOADED - assert entry.version == 1 - assert entry.minor_version == 2 + assert entry.version == 2 + assert entry.minor_version == 1 + # Migration to version 2.1 changed from/to to use station signatures + assert entry.data == { + "api_key": "1234567890", + "from": "Cst", + "to": "U", + "time": None, + "weekday": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], + "name": "Stockholm C to Uppsala C", + } + # Migration to version 1.2 removed unique_id assert entry.unique_id is None @@ -201,18 +222,73 @@ async def test_migrate_entry_from_future_version_fails( source=SOURCE_USER, data=ENTRY_CONFIG, options=OPTIONS_CONFIG, - version=2, + version=3, + minor_version=1, + entry_id="1", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.MIGRATION_ERROR + + +@pytest.mark.parametrize( + ("side_effect"), + [ + (InvalidAuthentication), + (NoTrainStationFound), + (UnknownError), + (Exception), + ], +) +async def test_migrate_entry_fails(hass: HomeAssistant, side_effect: Exception) -> None: + """Test migrate entry fails.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + version=1, + minor_version=1, entry_id="1", ) entry.add_to_hass(hass) with ( patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_station", - ), - patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - return_value=get_trains, + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_search_train_stations", + side_effect=side_effect(), + ), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_migrate_entry_fails_multiple_stations( + hass: HomeAssistant, + get_multiple_train_stations: list[StationInfoModel], +) -> None: + """Test migrate entry fails on multiple stations found.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + options=OPTIONS_CONFIG, + version=1, + minor_version=1, + entry_id="1", + unique_id="321", + ) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_search_train_stations", + side_effect=get_multiple_train_stations, ), ): await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/trafikverket_weatherstation/__init__.py b/tests/components/trafikverket_weatherstation/__init__.py index 836ee919195..4639211ed31 100644 --- a/tests/components/trafikverket_weatherstation/__init__.py +++ b/tests/components/trafikverket_weatherstation/__init__.py @@ -1 +1,9 @@ """Tests for the Trafikverket weatherstation integration.""" + +from homeassistant.components.trafikverket_weatherstation.const import CONF_STATION +from homeassistant.const import CONF_API_KEY + +ENTRY_CONFIG = { + CONF_API_KEY: "1234567890", + CONF_STATION: "Arlanda", +} diff --git a/tests/components/trafikverket_weatherstation/conftest.py b/tests/components/trafikverket_weatherstation/conftest.py new file mode 100644 index 00000000000..bd31c31c86b --- /dev/null +++ b/tests/components/trafikverket_weatherstation/conftest.py @@ -0,0 +1,80 @@ +"""Fixtures for the Trafikverket Weatherstation integration.""" + +from __future__ import annotations + +from collections.abc import AsyncGenerator +from datetime import datetime +from unittest.mock import patch + +import pytest +from pytrafikverket import WeatherStationInfoModel + +from homeassistant.components.trafikverket_weatherstation.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def load_int( + hass: HomeAssistant, mock_response: WeatherStationInfoModel +) -> MockConfigEntry: + """Set up the Trafikverket Weatherstation integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + ) + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture(name="mock_response") +async def mock_weather_response( + get_data: WeatherStationInfoModel, +) -> AsyncGenerator[None]: + """Mock a successful response.""" + with patch( + "homeassistant.components.trafikverket_weatherstation.coordinator.TrafikverketWeather.async_get_weather", + return_value=get_data, + ): + yield + + +@pytest.fixture(name="get_data") +async def get_data_from_library(hass: HomeAssistant) -> WeatherStationInfoModel: + """Retrieve data from Trafikverket Weatherstation library.""" + return WeatherStationInfoModel( + station_name="Arlanda", + station_id="227", + road_temp=-3.4, + air_temp=-3.0, + dew_point=-5.0, + humidity=84.1, + visible_distance=20000.0, + precipitationtype="no", + raining=False, + snowing=False, + road_ice=None, + road_ice_depth=None, + road_snow=None, + road_snow_depth=None, + road_water=None, + road_water_depth=None, + road_water_equivalent_depth=None, + winddirection="202", + wind_height=6.0, + windforce=1.2, + windforcemax=2.3, + measure_time=datetime.fromisoformat("2024-12-30T23:00:03+01:00"), + precipitation_amount=0.0, + modified_time=datetime.fromisoformat("2024-12-30T22:03:45.143000+00:00"), + ) diff --git a/tests/components/trafikverket_weatherstation/snapshots/test_diagnostics.ambr b/tests/components/trafikverket_weatherstation/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..95caa014d45 --- /dev/null +++ b/tests/components/trafikverket_weatherstation/snapshots/test_diagnostics.ambr @@ -0,0 +1,29 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'air_temp': -3.0, + 'dew_point': -5.0, + 'humidity': 84.1, + 'measure_time': '2024-12-30T23:00:03+01:00', + 'modified_time': '2024-12-30T22:03:45.143000+00:00', + 'precipitation_amount': 0.0, + 'precipitationtype': 'no', + 'raining': False, + 'road_ice': None, + 'road_ice_depth': None, + 'road_snow': None, + 'road_snow_depth': None, + 'road_temp': -3.4, + 'road_water': None, + 'road_water_depth': None, + 'road_water_equivalent_depth': None, + 'snowing': False, + 'station_id': '227', + 'station_name': 'Arlanda', + 'visible_distance': 20000.0, + 'wind_height': 6.0, + 'winddirection': '202', + 'windforce': 1.2, + 'windforcemax': 2.3, + }) +# --- diff --git a/tests/components/trafikverket_weatherstation/test_diagnostics.py b/tests/components/trafikverket_weatherstation/test_diagnostics.py new file mode 100644 index 00000000000..29b7b906dd0 --- /dev/null +++ b/tests/components/trafikverket_weatherstation/test_diagnostics.py @@ -0,0 +1,23 @@ +"""Test Trafikverket Weatherstation diagnostics.""" + +from __future__ import annotations + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + load_int: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test generating diagnostics for a config entry.""" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, load_int) == snapshot + ) diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index b1eae12d694..921cab4cba2 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -24,7 +24,7 @@ from homeassistant.components.tts import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component @@ -249,7 +249,7 @@ async def mock_config_entry_setup( async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test tts platform via config entry.""" async_add_entities([tts_entity]) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 0b01a24720d..d115546c9bc 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -22,9 +22,8 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import UNDEFINED from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import ( DEFAULT_LANG, @@ -64,7 +63,6 @@ async def test_default_entity_attributes() -> None: entity = DefaultEntity() assert entity.hass is None - assert entity.name is UNDEFINED assert entity.default_language == DEFAULT_LANG assert entity.supported_languages == SUPPORT_LANGUAGES assert entity.supported_options is None diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 1df4beb4232..0576fcd6a70 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -51,6 +51,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -81,6 +82,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://www.twentemilieu.nl', 'connections': set({ }), diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index 86ffc171082..b40ac0ba9e6 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -20,6 +20,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -50,6 +51,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://www.twentemilieu.nl', 'connections': set({ }), @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -129,6 +132,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://www.twentemilieu.nl', 'connections': set({ }), @@ -178,6 +182,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -208,6 +213,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://www.twentemilieu.nl', 'connections': set({ }), @@ -257,6 +263,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -287,6 +294,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://www.twentemilieu.nl', 'connections': set({ }), @@ -336,6 +344,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -366,6 +375,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://www.twentemilieu.nl', 'connections': set({ }), diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py index f322004962a..7b0ca20fbe1 100644 --- a/tests/components/twinkly/__init__.py +++ b/tests/components/twinkly/__init__.py @@ -1,118 +1,13 @@ """Constants and mock for the twinkly component tests.""" -from aiohttp.client_exceptions import ClientConnectionError +from homeassistant.core import HomeAssistant -from homeassistant.components.twinkly.const import DEV_NAME - -TEST_HOST = "test.twinkly.com" -TEST_ID = "twinkly_test_device_id" -TEST_UID = "4c8fccf5-e08a-4173-92d5-49bf479252a2" -TEST_NAME = "twinkly_test_device_name" -TEST_NAME_ORIGINAL = "twinkly_test_original_device_name" # the original (deprecated) name stored in the conf -TEST_MODEL = "twinkly_test_device_model" +from tests.common import MockConfigEntry -class ClientMock: - """A mock of the ttls.client.Twinkly.""" +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) - def __init__(self) -> None: - """Create a mocked client.""" - self.is_offline = False - self.state = True - self.brightness = {"mode": "enabled", "value": 10} - self.color = None - self.movies = [{"id": 1, "name": "Rainbow"}, {"id": 2, "name": "Flare"}] - self.current_movie = {} - self.default_mode = "movie" - self.mode = None - self.version = "2.8.10" - - self.id = TEST_UID - self.device_info = { - "uuid": self.id, - "device_name": TEST_NAME, - "product_code": TEST_MODEL, - } - - @property - def host(self) -> str: - """Get the mocked host.""" - return TEST_HOST - - async def get_details(self): - """Get the mocked device info.""" - if self.is_offline: - raise ClientConnectionError - return self.device_info - - async def is_on(self) -> bool: - """Get the mocked on/off state.""" - if self.is_offline: - raise ClientConnectionError - return self.state - - async def turn_on(self) -> None: - """Set the mocked on state.""" - if self.is_offline: - raise ClientConnectionError - self.state = True - self.mode = self.default_mode - - async def turn_off(self) -> None: - """Set the mocked off state.""" - if self.is_offline: - raise ClientConnectionError - self.state = False - - async def get_brightness(self) -> int: - """Get the mocked brightness.""" - if self.is_offline: - raise ClientConnectionError - return self.brightness - - async def set_brightness(self, brightness: int) -> None: - """Set the mocked brightness.""" - if self.is_offline: - raise ClientConnectionError - self.brightness = {"mode": "enabled", "value": brightness} - - def change_name(self, new_name: str) -> None: - """Change the name of this virtual device.""" - self.device_info[DEV_NAME] = new_name - - async def set_static_colour(self, colour) -> None: - """Set static color.""" - self.color = colour - self.default_mode = "color" - - async def set_cycle_colours(self, colour) -> None: - """Set static color.""" - self.color = colour - self.default_mode = "movie" - - async def interview(self) -> None: - """Interview.""" - - async def get_saved_movies(self) -> dict: - """Get saved movies.""" - return self.movies - - async def get_current_movie(self) -> dict: - """Get current movie.""" - return self.current_movie - - async def set_current_movie(self, movie_id: int) -> dict: - """Set current movie.""" - self.current_movie = {"id": movie_id} - - async def set_mode(self, mode: str) -> None: - """Set mode.""" - if mode == "off": - await self.turn_off() - else: - await self.turn_on() - self.mode = mode - - async def get_firmware_version(self) -> dict: - """Get firmware version.""" - return {"version": self.version} + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/twinkly/conftest.py b/tests/components/twinkly/conftest.py index 19361af2003..33b8f82a488 100644 --- a/tests/components/twinkly/conftest.py +++ b/tests/components/twinkly/conftest.py @@ -1,55 +1,71 @@ """Configure tests for the Twinkly integration.""" -from collections.abc import Awaitable, Callable, Coroutine -from typing import Any -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch import pytest -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.components.twinkly import DOMAIN +from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME -from . import TEST_MODEL, TEST_NAME, TEST_UID, ClientMock +from .const import TEST_MAC, TEST_MODEL, TEST_NAME -from tests.common import MockConfigEntry - -type ComponentSetup = Callable[[], Awaitable[ClientMock]] - -DOMAIN = "twinkly" -TITLE = "Twinkly" +from tests.common import MockConfigEntry, load_json_object_fixture -@pytest.fixture(name="config_entry") +@pytest.fixture def mock_config_entry() -> MockConfigEntry: """Create Twinkly entry in Home Assistant.""" - client = ClientMock() return MockConfigEntry( domain=DOMAIN, - title=TITLE, - unique_id=TEST_UID, - entry_id=TEST_UID, + title="Twinkly", + unique_id=TEST_MAC, data={ - "host": client.host, - "id": client.id, - "name": TEST_NAME, - "model": TEST_MODEL, - "device_name": TEST_NAME, + CONF_HOST: "192.168.0.123", + CONF_ID: "497dcba3-ecbf-4587-a2dd-5eb0665e6880", + CONF_NAME: TEST_NAME, + CONF_MODEL: TEST_MODEL, }, + entry_id="01JFMME2P6RA38V5AMPCJ2JYYV", + minor_version=2, ) -@pytest.fixture(name="setup_integration") -async def mock_setup_integration( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> Callable[[], Coroutine[Any, Any, ClientMock]]: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) +@pytest.fixture +def mock_twinkly_client() -> Generator[AsyncMock]: + """Mock the Twinkly client.""" + with ( + patch( + "homeassistant.components.twinkly.Twinkly", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.twinkly.config_flow.Twinkly", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_details.return_value = load_json_object_fixture( + "get_details.json", DOMAIN + ) + client.get_firmware_version.return_value = load_json_object_fixture( + "get_firmware_version.json", DOMAIN + ) + client.get_saved_movies.return_value = load_json_object_fixture( + "get_saved_movies.json", DOMAIN + ) + client.get_current_movie.return_value = load_json_object_fixture( + "get_current_movie.json", DOMAIN + ) + client.get_mode.return_value = load_json_object_fixture("get_mode.json", DOMAIN) + client.is_on.return_value = True + client.get_brightness.return_value = {"mode": "enabled", "value": 10} + client.host = "192.168.0.123" + yield client - async def func() -> ClientMock: - mock = ClientMock() - with patch("homeassistant.components.twinkly.Twinkly", return_value=mock): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - return mock - return func +@pytest.fixture +def mock_setup_entry() -> Generator[None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.twinkly.async_setup_entry", return_value=True): + yield diff --git a/tests/components/twinkly/const.py b/tests/components/twinkly/const.py new file mode 100644 index 00000000000..c2530f1a19d --- /dev/null +++ b/tests/components/twinkly/const.py @@ -0,0 +1,5 @@ +"""Constants for the Twinkly tests.""" + +TEST_MAC = "00:2d:13:3b:aa:bb" +TEST_NAME = "Tree 1" +TEST_MODEL = "TW2016" diff --git a/tests/components/twinkly/fixtures/get_current_movie.json b/tests/components/twinkly/fixtures/get_current_movie.json new file mode 100644 index 00000000000..2572ae5fe7c --- /dev/null +++ b/tests/components/twinkly/fixtures/get_current_movie.json @@ -0,0 +1,3 @@ +{ + "id": 1 +} diff --git a/tests/components/twinkly/fixtures/get_details.json b/tests/components/twinkly/fixtures/get_details.json new file mode 100644 index 00000000000..1519520b0b9 --- /dev/null +++ b/tests/components/twinkly/fixtures/get_details.json @@ -0,0 +1,23 @@ +{ + "product_name": "Twinkly", + "product_version": "1", + "hardware_version": "1", + "flash_size": 4, + "led_type": 1, + "led_version": "1", + "product_code": "TW2016", + "device_name": "Tree 1", + "uptime": "4087441", + "rssi": -78, + "hw_id": "002d133b", + "mac": "00:2d:13:3b:aa:bb", + "uuid": "00000000-0000-0000-0000-000000000000", + "max_supported_led": 100, + "base_leds_number": 100, + "number_of_led": 100, + "led_profile": "RGB", + "frame_rate": 14, + "movie_capacity": 708, + "copyright": "LEDWORKS 2017", + "code": 1000 +} diff --git a/tests/components/twinkly/fixtures/get_firmware_version.json b/tests/components/twinkly/fixtures/get_firmware_version.json new file mode 100644 index 00000000000..4f3df8b9ed1 --- /dev/null +++ b/tests/components/twinkly/fixtures/get_firmware_version.json @@ -0,0 +1 @@ +{ "version": "2.7.2" } diff --git a/tests/components/twinkly/fixtures/get_mode.json b/tests/components/twinkly/fixtures/get_mode.json new file mode 100644 index 00000000000..38aed254a62 --- /dev/null +++ b/tests/components/twinkly/fixtures/get_mode.json @@ -0,0 +1,3 @@ +{ + "mode": "color" +} diff --git a/tests/components/twinkly/fixtures/get_saved_movies.json b/tests/components/twinkly/fixtures/get_saved_movies.json new file mode 100644 index 00000000000..0fa7696d3df --- /dev/null +++ b/tests/components/twinkly/fixtures/get_saved_movies.json @@ -0,0 +1,12 @@ +{ + "movies": [ + { + "id": 1, + "name": "Rainbow" + }, + { + "id": 2, + "name": "Flare" + } + ] +} diff --git a/tests/components/twinkly/snapshots/test_diagnostics.ambr b/tests/components/twinkly/snapshots/test_diagnostics.ambr index e52f76634fd..511bf9addd3 100644 --- a/tests/components/twinkly/snapshots/test_diagnostics.ambr +++ b/tests/components/twinkly/snapshots/test_diagnostics.ambr @@ -3,35 +3,67 @@ dict({ 'attributes': dict({ 'brightness': 26, - 'color_mode': 'brightness', - 'effect': None, + 'color_mode': 'rgb', + 'effect': '1 Rainbow', 'effect_list': list([ + '1 Rainbow', + '2 Flare', + ]), + 'friendly_name': 'Tree 1', + 'hs_color': list([ + 0.0, + 0.0, + ]), + 'rgb_color': list([ + 255, + 255, + 255, ]), - 'friendly_name': 'twinkly_test_device_name', 'supported_color_modes': list([ - 'brightness', + 'rgb', ]), 'supported_features': 4, + 'xy_color': list([ + 0.323, + 0.329, + ]), }), 'device_info': dict({ - 'device_name': 'twinkly_test_device_name', - 'product_code': 'twinkly_test_device_model', - 'uuid': '4c8fccf5-e08a-4173-92d5-49bf479252a2', + 'base_leds_number': 100, + 'code': 1000, + 'copyright': 'LEDWORKS 2017', + 'device_name': 'Tree 1', + 'flash_size': 4, + 'frame_rate': 14, + 'hardware_version': '1', + 'hw_id': '002d133b', + 'led_profile': 'RGB', + 'led_type': 1, + 'led_version': '1', + 'mac': '**REDACTED**', + 'max_supported_led': 100, + 'movie_capacity': 708, + 'number_of_led': 100, + 'product_code': 'TW2016', + 'product_name': 'Twinkly', + 'product_version': '1', + 'rssi': -78, + 'uptime': '4087441', + 'uuid': '00000000-0000-0000-0000-000000000000', }), 'entry': dict({ 'data': dict({ - 'device_name': 'twinkly_test_device_name', 'host': '**REDACTED**', - 'id': '4c8fccf5-e08a-4173-92d5-49bf479252a2', - 'model': 'twinkly_test_device_model', - 'name': 'twinkly_test_device_name', + 'id': '497dcba3-ecbf-4587-a2dd-5eb0665e6880', + 'model': 'TW2016', + 'name': 'Tree 1', }), 'disabled_by': None, 'discovery_keys': dict({ }), 'domain': 'twinkly', - 'entry_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2', - 'minor_version': 1, + 'entry_id': '01JFMME2P6RA38V5AMPCJ2JYYV', + 'minor_version': 2, 'options': dict({ }), 'pref_disable_new_entities': False, @@ -40,9 +72,9 @@ 'subentries': list([ ]), 'title': 'Twinkly', - 'unique_id': '4c8fccf5-e08a-4173-92d5-49bf479252a2', + 'unique_id': '00:2d:13:3b:aa:bb', 'version': 1, }), - 'sw_version': '2.8.10', + 'sw_version': '2.7.2', }) # --- diff --git a/tests/components/twinkly/snapshots/test_light.ambr b/tests/components/twinkly/snapshots/test_light.ambr new file mode 100644 index 00000000000..77a97a0cdd9 --- /dev/null +++ b/tests/components/twinkly/snapshots/test_light.ambr @@ -0,0 +1,80 @@ +# serializer version: 1 +# name: test_entities[light.tree_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + '1 Rainbow', + '2 Flare', + ]), + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.tree_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'twinkly', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'light', + 'unique_id': '00:2d:13:3b:aa:bb', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[light.tree_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 26, + 'color_mode': , + 'effect': '1 Rainbow', + 'effect_list': list([ + '1 Rainbow', + '2 Flare', + ]), + 'friendly_name': 'Tree 1', + 'hs_color': tuple( + 0.0, + 0.0, + ), + 'rgb_color': tuple( + 255, + 255, + 255, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.323, + 0.329, + ), + }), + 'context': , + 'entity_id': 'light.tree_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/twinkly/snapshots/test_select.ambr b/tests/components/twinkly/snapshots/test_select.ambr new file mode 100644 index 00000000000..26edd4b731d --- /dev/null +++ b/tests/components/twinkly/snapshots/test_select.ambr @@ -0,0 +1,67 @@ +# serializer version: 1 +# name: test_select_entities[select.tree_1_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'color', + 'demo', + 'effect', + 'movie', + 'off', + 'playlist', + 'rt', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.tree_1_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mode', + 'platform': 'twinkly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00:2d:13:3b:aa:bb_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select_entities[select.tree_1_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Tree 1 Mode', + 'options': list([ + 'color', + 'demo', + 'effect', + 'movie', + 'off', + 'playlist', + 'rt', + ]), + }), + 'context': , + 'entity_id': 'select.tree_1_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'color', + }) +# --- diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index 9b9aeafd082..352c5249b0b 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -1,159 +1,170 @@ """Tests for the config_flow of the twinly component.""" -from unittest.mock import patch +from unittest.mock import AsyncMock -from homeassistant import config_entries -from homeassistant.components import dhcp -from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN +import pytest + +from homeassistant.components.twinkly.const import DOMAIN +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo -from . import TEST_MODEL, TEST_NAME, ClientMock +from .const import TEST_MAC, TEST_MODEL, TEST_NAME from tests.common import MockConfigEntry -async def test_invalid_host(hass: HomeAssistant) -> None: - """Test the failure when invalid host provided.""" - client = ClientMock() - client.is_offline = True - with patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ): - result = await hass.config_entries.flow.async_init( - TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "dummy"}, - ) +@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry") +async def test_full_flow(hass: HomeAssistant) -> None: + """Test the full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.123"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_NAME + assert result["data"] == { + CONF_HOST: "192.168.0.123", + CONF_ID: "00000000-0000-0000-0000-000000000000", + CONF_NAME: TEST_NAME, + CONF_MODEL: TEST_MODEL, + } + assert result["result"].unique_id == TEST_MAC + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_exceptions(hass: HomeAssistant, mock_twinkly_client: AsyncMock) -> None: + """Test the failure when raising exceptions.""" + mock_twinkly_client.get_details.side_effect = TimeoutError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.123"}, + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_HOST: "cannot_connect"} + mock_twinkly_client.get_details.side_effect = None -async def test_success_flow(hass: HomeAssistant) -> None: - """Test that an entity is created when the flow completes.""" - client = ClientMock() - with ( - patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ), - patch("homeassistant.components.twinkly.async_setup_entry", return_value=True), - ): - result = await hass.config_entries.flow.async_init( - TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "dummy"}, - ) - + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "192.168.0.123"}, + ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == TEST_NAME - assert result["data"] == { - CONF_HOST: "dummy", - CONF_ID: client.id, - CONF_NAME: TEST_NAME, - CONF_MODEL: TEST_MODEL, - } -async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry") +async def test_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the device is already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "192.168.0.123"} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry") +async def test_dhcp_full_flow(hass: HomeAssistant) -> None: """Test DHCP discovery flow can confirm right away.""" - client = ClientMock() - with patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ): - result = await hass.config_entries.flow.async_init( - TWINKLY_DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - hostname="Twinkly_XYZ", - ip="1.2.3.4", - macaddress="aabbccddeeff", - ), - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="Twinkly_XYZ", + ip="1.2.3.4", + macaddress="002d133baabb", + ), + ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" - -async def test_dhcp_success(hass: HomeAssistant) -> None: - """Test DHCP discovery flow success.""" - client = ClientMock() - with ( - patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ), - patch("homeassistant.components.twinkly.async_setup_entry", return_value=True), - ): - result = await hass.config_entries.flow.async_init( - TWINKLY_DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - hostname="Twinkly_XYZ", - ip="1.2.3.4", - macaddress="aabbccddeeff", - ), - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "discovery_confirm" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TEST_NAME assert result["data"] == { CONF_HOST: "1.2.3.4", - CONF_ID: client.id, + CONF_ID: "00000000-0000-0000-0000-000000000000", CONF_NAME: TEST_NAME, CONF_MODEL: TEST_MODEL, } + assert result["result"].unique_id == TEST_MAC -async def test_dhcp_already_exists(hass: HomeAssistant) -> None: - """Test DHCP discovery flow that fails to connect.""" - client = ClientMock() +@pytest.mark.usefixtures("mock_twinkly_client") +async def test_dhcp_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test DHCP discovery flow aborts if entry already setup.""" + mock_config_entry.add_to_hass(hass) - entry = MockConfigEntry( - domain=TWINKLY_DOMAIN, - data={ - CONF_HOST: "1.2.3.4", - CONF_ID: client.id, - CONF_NAME: TEST_NAME, - CONF_MODEL: TEST_MODEL, - }, - unique_id=client.id, + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="Twinkly_XYZ", + ip="1.2.3.4", + macaddress="002d133baabb", + ), ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ): - result = await hass.config_entries.flow.async_init( - TWINKLY_DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - hostname="Twinkly_XYZ", - ip="1.2.3.4", - macaddress="aabbccddeeff", - ), - ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" + + +@pytest.mark.usefixtures("mock_twinkly_client", "mock_setup_entry") +async def test_user_flow_works_discovery(hass: HomeAssistant) -> None: + """Test user flow can continue after discovery happened.""" + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="Twinkly_XYZ", + ip="1.2.3.4", + macaddress="002d133baabb", + ), + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 2 + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "10.0.0.131"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # Verify the discovery flow was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/twinkly/test_diagnostics.py b/tests/components/twinkly/test_diagnostics.py index f9cf0bc562c..d7ef4dd9b11 100644 --- a/tests/components/twinkly/test_diagnostics.py +++ b/tests/components/twinkly/test_diagnostics.py @@ -1,32 +1,28 @@ """Tests for the diagnostics of the twinkly component.""" -from collections.abc import Awaitable, Callable - +import pytest from syrupy import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant -from . import ClientMock +from . import setup_integration +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator -type ComponentSetup = Callable[[], Awaitable[ClientMock]] - -DOMAIN = "twinkly" - +@pytest.mark.usefixtures("mock_twinkly_client") async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - setup_integration: ComponentSetup, + mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" - await setup_integration() - entry = hass.config_entries.async_entries(DOMAIN)[0] + await setup_integration(hass, mock_config_entry) - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( - exclude=props("created_at", "modified_at") - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index 6642807ac3f..0a76a399b63 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -1,64 +1,86 @@ -"""Tests of the initialization of the twinly integration.""" +"""Tests of the initialization of the twinkly integration.""" -from unittest.mock import patch -from uuid import uuid4 +from unittest.mock import AsyncMock -from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN +from aiohttp import ClientConnectionError +import pytest + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.twinkly.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL, ClientMock +from . import setup_integration +from .const import TEST_MAC, TEST_MODEL from tests.common import MockConfigEntry -async def test_load_unload_entry(hass: HomeAssistant) -> None: - """Validate that setup entry also configure the client.""" - client = ClientMock() +@pytest.mark.usefixtures("mock_twinkly_client") +async def test_load_unload_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the load/unload of the config entry.""" - device_id = str(uuid4()) + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: + """Validate that config entry is retried.""" + mock_twinkly_client.get_details.side_effect = ClientConnectionError + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.usefixtures("mock_twinkly_client") +async def test_mac_migration( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Validate that the unique_id is migrated to the MAC address.""" config_entry = MockConfigEntry( - domain=TWINKLY_DOMAIN, + domain=DOMAIN, + minor_version=1, + unique_id="unique_id", data={ - CONF_HOST: TEST_HOST, - CONF_ID: device_id, - CONF_NAME: TEST_NAME_ORIGINAL, + CONF_HOST: "192.168.0.123", + CONF_ID: id, + CONF_NAME: "Tree 1", CONF_MODEL: TEST_MODEL, }, - entry_id=device_id, + ) + config_entry.add_to_hass(hass) + entity_entry = entity_registry.async_get_or_create( + LIGHT_DOMAIN, + DOMAIN, + config_entry.unique_id, + ) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, config_entry.unique_id)}, ) - config_entry.add_to_hass(hass) - - with patch("homeassistant.components.twinkly.Twinkly", return_value=client): - await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.LOADED - await hass.config_entries.async_unload(config_entry.entry_id) - - assert config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_config_entry_not_ready(hass: HomeAssistant) -> None: - """Validate that config entry is retried.""" - client = ClientMock() - client.is_offline = True - - config_entry = MockConfigEntry( - domain=TWINKLY_DOMAIN, - data={ - CONF_HOST: TEST_HOST, - CONF_ID: id, - CONF_NAME: TEST_NAME_ORIGINAL, - CONF_MODEL: TEST_MODEL, - }, - ) - - config_entry.add_to_hass(hass) - - with patch("homeassistant.components.twinkly.Twinkly", return_value=client): - await hass.config_entries.async_setup(config_entry.entry_id) - - assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert entity_registry.async_get(entity_entry.entity_id).unique_id == TEST_MAC + assert device_registry.async_get_device( + identifiers={(DOMAIN, config_entry.unique_id)} + ).identifiers == {(DOMAIN, TEST_MAC)} + assert config_entry.unique_id == TEST_MAC diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 7a55dbec14a..f8289cb95e3 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -3,290 +3,311 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from typing import Any +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion +from ttls.client import TwinklyError -from homeassistant.components.light import ATTR_BRIGHTNESS, LightEntityFeature -from homeassistant.components.twinkly.const import DOMAIN as TWINKLY_DOMAIN -from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + DOMAIN as LIGHT_DOMAIN, + LightEntityFeature, +) +from homeassistant.components.twinkly import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.entity_registry import RegistryEntry -from . import TEST_MODEL, TEST_NAME, TEST_NAME_ORIGINAL, ClientMock +from . import setup_integration +from .const import TEST_MAC -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform -async def test_initial_state(hass: HomeAssistant) -> None: - """Validate that entity and device states are updated on startup.""" - entity, device, _, _ = await _create_entries(hass) +@pytest.mark.usefixtures("mock_twinkly_client") +async def test_entities( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the created entities.""" + with patch("homeassistant.components.twinkly.PLATFORMS", [Platform.LIGHT]): + await setup_integration(hass, mock_config_entry) - state = hass.states.get(entity.entity_id) - - # Basic state properties - assert state.name == TEST_NAME - assert state.state == "on" - assert state.attributes[ATTR_BRIGHTNESS] == 26 - assert state.attributes["friendly_name"] == TEST_NAME - - assert device.name == TEST_NAME - assert device.model == TEST_MODEL - assert device.manufacturer == "LEDWORKS" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_turn_on_off(hass: HomeAssistant) -> None: +async def test_turn_on_off( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: """Test support of the light.turn_on service.""" - client = ClientMock() - client.state = False - client.brightness = {"mode": "enabled", "value": 20} - entity, _, _, _ = await _create_entries(hass, client) + mock_twinkly_client.is_on.return_value = False - assert hass.states.get(entity.entity_id).state == "off" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("light.tree_1").state == STATE_OFF await hass.services.async_call( - "light", "turn_on", service_data={"entity_id": entity.entity_id}, blocking=True + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1"}, + blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert state.attributes[ATTR_BRIGHTNESS] == 51 + mock_twinkly_client.turn_on.assert_called_once_with() -async def test_turn_on_with_brightness(hass: HomeAssistant) -> None: +async def test_turn_on_with_brightness( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: """Test support of the light.turn_on service with a brightness parameter.""" - client = ClientMock() - client.state = False - client.brightness = {"mode": "enabled", "value": 20} - entity, _, _, _ = await _create_entries(hass, client) + mock_twinkly_client.is_on.return_value = False - assert hass.states.get(entity.entity_id).state == "off" + await setup_integration(hass, mock_config_entry) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "brightness": 255}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_BRIGHTNESS: 255}, blocking=True, ) - state = hass.states.get(entity.entity_id) + mock_twinkly_client.set_brightness.assert_called_once_with(100) + mock_twinkly_client.turn_on.assert_called_once_with() - assert state.state == "on" - assert state.attributes[ATTR_BRIGHTNESS] == 255 + +async def test_brightness_to_zero( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: + """Test support of the light.turn_on service with a brightness parameter.""" + await setup_integration(hass, mock_config_entry) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "brightness": 1}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_BRIGHTNESS: 1}, blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "off" + mock_twinkly_client.set_brightness.assert_not_called() + mock_twinkly_client.turn_off.assert_called_once_with() -async def test_turn_on_with_color_rgbw(hass: HomeAssistant) -> None: +async def test_turn_on_with_color_rgbw( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: """Test support of the light.turn_on service with a rgbw parameter.""" - client = ClientMock() - client.state = False - client.device_info["led_profile"] = "RGBW" - client.brightness = {"mode": "enabled", "value": 255} - entity, _, _, _ = await _create_entries(hass, client) + mock_twinkly_client.is_on.return_value = False + mock_twinkly_client.get_details.return_value["led_profile"] = "RGBW" - assert hass.states.get(entity.entity_id).state == "off" + await setup_integration(hass, mock_config_entry) assert ( LightEntityFeature.EFFECT - & hass.states.get(entity.entity_id).attributes["supported_features"] + & hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES] ) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ + ATTR_ENTITY_ID: "light.tree_1", + ATTR_RGBW_COLOR: (128, 64, 32, 0), + }, blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert client.color == (128, 64, 32) - assert client.default_mode == "color" - assert client.mode == "color" + mock_twinkly_client.interview.assert_called_once_with() + mock_twinkly_client.set_static_colour.assert_called_once_with((0, 128, 64, 32)) + mock_twinkly_client.set_mode.assert_called_once_with("color") + assert mock_twinkly_client.default_mode == "color" -async def test_turn_on_with_color_rgb(hass: HomeAssistant) -> None: +async def test_turn_on_with_color_rgb( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: """Test support of the light.turn_on service with a rgb parameter.""" - client = ClientMock() - client.state = False - client.device_info["led_profile"] = "RGB" - client.brightness = {"mode": "enabled", "value": 255} - entity, _, _, _ = await _create_entries(hass, client) + mock_twinkly_client.is_on.return_value = False + mock_twinkly_client.get_details.return_value["led_profile"] = "RGB" - assert hass.states.get(entity.entity_id).state == "off" + await setup_integration(hass, mock_config_entry) assert ( LightEntityFeature.EFFECT - & hass.states.get(entity.entity_id).attributes["supported_features"] + & hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES] ) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_RGB_COLOR: (128, 64, 32)}, blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert client.color == (128, 64, 32) - assert client.default_mode == "color" - assert client.mode == "color" + mock_twinkly_client.interview.assert_called_once_with() + mock_twinkly_client.set_static_colour.assert_called_once_with((128, 64, 32)) + mock_twinkly_client.set_mode.assert_called_once_with("color") + assert mock_twinkly_client.default_mode == "color" -async def test_turn_on_with_effect(hass: HomeAssistant) -> None: +async def test_turn_on_with_effect( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: """Test support of the light.turn_on service with effects.""" - client = ClientMock() - client.state = False - client.device_info["led_profile"] = "RGB" - client.brightness = {"mode": "enabled", "value": 255} - entity, _, _, _ = await _create_entries(hass, client) + mock_twinkly_client.is_on.return_value = False + mock_twinkly_client.get_details.return_value["led_profile"] = "RGB" - assert hass.states.get(entity.entity_id).state == "off" - assert not client.current_movie + await setup_integration(hass, mock_config_entry) assert ( LightEntityFeature.EFFECT - & hass.states.get(entity.entity_id).attributes["supported_features"] + & hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES] ) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_EFFECT: "2 Rainbow"}, blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert client.current_movie["id"] == 1 - assert client.default_mode == "movie" - assert client.mode == "movie" + mock_twinkly_client.interview.assert_called_once_with() + mock_twinkly_client.set_current_movie.assert_called_once_with(2) + mock_twinkly_client.set_mode.assert_called_once_with("movie") + assert mock_twinkly_client.default_mode == "movie" -async def test_turn_on_with_color_rgbw_and_missing_effect(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("data"), + [ + {ATTR_RGBW_COLOR: (128, 64, 32, 0)}, + {ATTR_RGB_COLOR: (128, 64, 32)}, + ], +) +async def test_turn_on_with_missing_effect( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, + data: dict[str, Any], +) -> None: """Test support of the light.turn_on service with rgbw color and missing effect support.""" - client = ClientMock() - client.state = False - client.device_info["led_profile"] = "RGBW" - client.brightness = {"mode": "enabled", "value": 255} - client.version = "2.7.0" - entity, _, _, _ = await _create_entries(hass, client) + mock_twinkly_client.is_on.return_value = False + mock_twinkly_client.get_firmware_version.return_value["version"] = "2.7.0" - assert hass.states.get(entity.entity_id).state == "off" + await setup_integration(hass, mock_config_entry) assert ( - not LightEntityFeature.EFFECT - & hass.states.get(entity.entity_id).attributes["supported_features"] + LightEntityFeature.EFFECT + ^ hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES] ) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1"} | data, blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert client.color == (0, 128, 64, 32) - assert client.mode == "movie" - assert client.default_mode == "movie" + mock_twinkly_client.interview.assert_called_once_with() + mock_twinkly_client.set_cycle_colours.assert_called_once_with((128, 64, 32)) + mock_twinkly_client.set_mode.assert_called_once_with("movie") + assert mock_twinkly_client.default_mode == "movie" + mock_twinkly_client.set_current_movie.assert_not_called() -async def test_turn_on_with_color_rgb_and_missing_effect(hass: HomeAssistant) -> None: - """Test support of the light.turn_on service with rgb color and missing effect support.""" - client = ClientMock() - client.state = False - client.device_info["led_profile"] = "RGB" - client.brightness = {"mode": "enabled", "value": 255} - client.version = "2.7.0" - entity, _, _, _ = await _create_entries(hass, client) +async def test_turn_on_with_color_rgbw_and_missing_effect( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: + """Test support of the light.turn_on service with missing effect support.""" + mock_twinkly_client.is_on.return_value = False + mock_twinkly_client.get_firmware_version.return_value["version"] = "2.7.0" - assert hass.states.get(entity.entity_id).state == "off" + await setup_integration(hass, mock_config_entry) assert ( - not LightEntityFeature.EFFECT - & hass.states.get(entity.entity_id).attributes["supported_features"] + LightEntityFeature.EFFECT + ^ hass.states.get("light.tree_1").attributes[ATTR_SUPPORTED_FEATURES] ) await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)}, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: "light.tree_1", ATTR_EFFECT: "2 Rainbow"}, blocking=True, ) - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert client.color == (128, 64, 32) - assert client.mode == "movie" - assert client.default_mode == "movie" + mock_twinkly_client.set_current_movie.assert_not_called() -async def test_turn_on_with_effect_missing_effects(hass: HomeAssistant) -> None: - """Test support of the light.turn_on service with effect set even if effects are not supported.""" - client = ClientMock() - client.state = False - client.device_info["led_profile"] = "RGB" - client.brightness = {"mode": "enabled", "value": 255} - client.version = "2.7.0" - entity, _, _, _ = await _create_entries(hass, client) - - assert hass.states.get(entity.entity_id).state == "off" - assert not client.current_movie - assert ( - not LightEntityFeature.EFFECT - & hass.states.get(entity.entity_id).attributes["supported_features"] - ) - - await hass.services.async_call( - "light", - "turn_on", - service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"}, - blocking=True, - ) - - state = hass.states.get(entity.entity_id) - - assert state.state == "on" - assert not client.current_movie - assert client.default_mode == "movie" - assert client.mode == "movie" - - -async def test_turn_off(hass: HomeAssistant) -> None: +async def test_turn_off( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: """Test support of the light.turn_off service.""" - entity, _, _, _ = await _create_entries(hass) - - assert hass.states.get(entity.entity_id).state == "on" + await setup_integration(hass, mock_config_entry) await hass.services.async_call( - "light", "turn_off", service_data={"entity_id": entity.entity_id}, blocking=True + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + service_data={ATTR_ENTITY_ID: "light.tree_1"}, + blocking=True, ) + mock_twinkly_client.turn_off.assert_called_once_with() - state = hass.states.get(entity.entity_id) - assert state.state == "off" +async def test_no_current_movie( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test handling of missing current movie data.""" + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("light.tree_1").attributes[ATTR_EFFECT] == "1 Rainbow" + + mock_twinkly_client.get_current_movie.side_effect = TwinklyError + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("light.tree_1").state != STATE_UNAVAILABLE + assert hass.states.get("light.tree_1").attributes[ATTR_EFFECT] is None async def test_update_name( hass: HomeAssistant, device_registry: dr.DeviceRegistry, freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, ) -> None: """Validate device's name update behavior. @@ -294,56 +315,19 @@ async def test_update_name( then the name of the entity is updated and it's also persisted, so it can be restored when starting HA while Twinkly is offline. """ - entity, _, client, config_entry = await _create_entries(hass) - client.change_name("new_device_name") + await setup_integration(hass, mock_config_entry) + + dev_entry = device_registry.async_get_device({(DOMAIN, TEST_MAC)}) + + assert dev_entry.name == "Tree 1" + + mock_twinkly_client.get_details.return_value["device_name"] = "new_device_name" + freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) await hass.async_block_till_done() - dev_entry = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}) + dev_entry = device_registry.async_get_device({(DOMAIN, TEST_MAC)}) assert dev_entry.name == "new_device_name" - assert config_entry.data[CONF_NAME] == "new_device_name" - - -async def test_unload(hass: HomeAssistant) -> None: - """Validate that entities can be unloaded from the UI.""" - - _, _, client, _ = await _create_entries(hass) - entry_id = client.id - - assert await hass.config_entries.async_unload(entry_id) - - -async def _create_entries( - hass: HomeAssistant, client=None -) -> tuple[RegistryEntry, DeviceEntry, ClientMock]: - client = ClientMock() if client is None else client - - with patch("homeassistant.components.twinkly.Twinkly", return_value=client): - config_entry = MockConfigEntry( - domain=TWINKLY_DOMAIN, - data={ - CONF_HOST: client, - CONF_ID: client.id, - CONF_NAME: TEST_NAME_ORIGINAL, - CONF_MODEL: TEST_MODEL, - }, - entry_id=client.id, - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(client.id) - await hass.async_block_till_done() - - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) - - entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id) - entity_entry = entity_registry.async_get(entity_id) - device = device_registry.async_get_device(identifiers={(TWINKLY_DOMAIN, client.id)}) - - assert entity_entry is not None - assert device is not None - - return entity_entry, device, client, config_entry diff --git a/tests/components/twinkly/test_select.py b/tests/components/twinkly/test_select.py new file mode 100644 index 00000000000..103fbe0f634 --- /dev/null +++ b/tests/components/twinkly/test_select.py @@ -0,0 +1,77 @@ +"""Tests for the Twinkly select component.""" + +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("mock_twinkly_client") +async def test_select_entities( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the created select entities.""" + with patch("homeassistant.components.twinkly.PLATFORMS", [Platform.SELECT]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_select_mode( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, +) -> None: + """Test selecting a mode.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.tree_1_mode") + assert state is not None + assert state.state == "color" + + await hass.services.async_call( + SELECT_DOMAIN, + "select_option", + { + ATTR_ENTITY_ID: "select.tree_1_mode", + ATTR_OPTION: "movie", + }, + blocking=True, + ) + + mock_twinkly_client.set_mode.assert_called_once_with("movie") + mock_twinkly_client.interview.assert_not_called() + + +async def test_mode_unavailable( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_twinkly_client: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test handling of unavailable mode data.""" + await setup_integration(hass, mock_config_entry) + + mock_twinkly_client.get_mode.side_effect = Exception + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("select.tree_1_mode") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 798b613b18d..ec7a0595731 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -26,7 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -174,6 +174,7 @@ def fixture_request( dpi_group_payload: list[dict[str, Any]], port_forward_payload: list[dict[str, Any]], traffic_rule_payload: list[dict[str, Any]], + traffic_route_payload: list[dict[str, Any]], site_payload: list[dict[str, Any]], system_information_payload: list[dict[str, Any]], wlan_payload: list[dict[str, Any]], @@ -214,6 +215,7 @@ def fixture_request( mock_get_request(f"/api/s/{site_id}/stat/sysinfo", system_information_payload) mock_get_request(f"/api/s/{site_id}/rest/wlanconf", wlan_payload) mock_get_request(f"/v2/api/site/{site_id}/trafficrules", traffic_rule_payload) + mock_get_request(f"/v2/api/site/{site_id}/trafficroutes", traffic_route_payload) return __mock_requests @@ -291,6 +293,12 @@ def traffic_rule_payload_data() -> list[dict[str, Any]]: return [] +@pytest.fixture(name="traffic_route_payload") +def traffic_route_payload_data() -> list[dict[str, Any]]: + """Traffic route data.""" + return [] + + @pytest.fixture(name="wlan_payload") def fixture_wlan_data() -> list[dict[str, Any]]: """WLAN data.""" diff --git a/tests/components/unifi/snapshots/test_button.ambr b/tests/components/unifi/snapshots/test_button.ambr index 3729bd31cf0..369b0823063 100644 --- a/tests/components/unifi/snapshots/test_button.ambr +++ b/tests/components/unifi/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/unifi/snapshots/test_device_tracker.ambr b/tests/components/unifi/snapshots/test_device_tracker.ambr index 3debd512050..5d3407e4e8e 100644 --- a/tests/components/unifi/snapshots/test_device_tracker.ambr +++ b/tests/components/unifi/snapshots/test_device_tracker.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,6 +56,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -104,6 +106,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/unifi/snapshots/test_image.ambr b/tests/components/unifi/snapshots/test_image.ambr index 32e1a5ff622..05cca2c305b 100644 --- a/tests/components/unifi/snapshots/test_image.ambr +++ b/tests/components/unifi/snapshots/test_image.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/unifi/snapshots/test_sensor.ambr b/tests/components/unifi/snapshots/test_sensor.ambr index fc86a57a294..4d109f630c5 100644 --- a/tests/components/unifi/snapshots/test_sensor.ambr +++ b/tests/components/unifi/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -55,22 +56,22 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'Disconnected', - 'Connected', - 'Pending', - 'Firmware Mismatch', - 'Upgrading', - 'Provisioning', - 'Heartbeat Missed', - 'Adopting', - 'Deleting', - 'Inform Error', - 'Adoption Failed', - 'Isolated', - 'Unknown', + 'disconnected', + 'connected', + 'pending', + 'firmware_mismatch', + 'upgrading', + 'provisioning', + 'heartbeat_missed', + 'adopting', + 'deleting', + 'inform_error', + 'adoption_failed', + 'isolated', ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -103,19 +104,18 @@ 'device_class': 'enum', 'friendly_name': 'Device State', 'options': list([ - 'Disconnected', - 'Connected', - 'Pending', - 'Firmware Mismatch', - 'Upgrading', - 'Provisioning', - 'Heartbeat Missed', - 'Adopting', - 'Deleting', - 'Inform Error', - 'Adoption Failed', - 'Isolated', - 'Unknown', + 'disconnected', + 'connected', + 'pending', + 'firmware_mismatch', + 'upgrading', + 'provisioning', + 'heartbeat_missed', + 'adopting', + 'deleting', + 'inform_error', + 'adoption_failed', + 'isolated', ]), }), 'context': , @@ -123,7 +123,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Connected', + 'state': 'connected', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.device_temperature-entry] @@ -133,6 +133,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -181,6 +182,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -230,6 +232,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -284,6 +287,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -338,6 +342,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -387,6 +392,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -437,6 +443,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -487,6 +494,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -536,22 +544,22 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'Disconnected', - 'Connected', - 'Pending', - 'Firmware Mismatch', - 'Upgrading', - 'Provisioning', - 'Heartbeat Missed', - 'Adopting', - 'Deleting', - 'Inform Error', - 'Adoption Failed', - 'Isolated', - 'Unknown', + 'disconnected', + 'connected', + 'pending', + 'firmware_mismatch', + 'upgrading', + 'provisioning', + 'heartbeat_missed', + 'adopting', + 'deleting', + 'inform_error', + 'adoption_failed', + 'isolated', ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -584,19 +592,18 @@ 'device_class': 'enum', 'friendly_name': 'Dummy USP-PDU-Pro State', 'options': list([ - 'Disconnected', - 'Connected', - 'Pending', - 'Firmware Mismatch', - 'Upgrading', - 'Provisioning', - 'Heartbeat Missed', - 'Adopting', - 'Deleting', - 'Inform Error', - 'Adoption Failed', - 'Isolated', - 'Unknown', + 'disconnected', + 'connected', + 'pending', + 'firmware_mismatch', + 'upgrading', + 'provisioning', + 'heartbeat_missed', + 'adopting', + 'deleting', + 'inform_error', + 'adoption_failed', + 'isolated', ]), }), 'context': , @@ -604,7 +611,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Connected', + 'state': 'connected', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.dummy_usp_pdu_pro_uptime-entry] @@ -614,6 +621,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -663,6 +671,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -712,6 +721,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -763,6 +773,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -814,6 +825,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -865,6 +877,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -916,6 +929,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -967,6 +981,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1018,6 +1033,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1069,6 +1085,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1123,6 +1140,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1177,6 +1195,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1228,6 +1247,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1282,6 +1302,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1336,6 +1357,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1390,6 +1412,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1444,6 +1467,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1495,6 +1519,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1549,6 +1574,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1601,22 +1627,22 @@ 'area_id': None, 'capabilities': dict({ 'options': list([ - 'Disconnected', - 'Connected', - 'Pending', - 'Firmware Mismatch', - 'Upgrading', - 'Provisioning', - 'Heartbeat Missed', - 'Adopting', - 'Deleting', - 'Inform Error', - 'Adoption Failed', - 'Isolated', - 'Unknown', + 'disconnected', + 'connected', + 'pending', + 'firmware_mismatch', + 'upgrading', + 'provisioning', + 'heartbeat_missed', + 'adopting', + 'deleting', + 'inform_error', + 'adoption_failed', + 'isolated', ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1649,19 +1675,18 @@ 'device_class': 'enum', 'friendly_name': 'mock-name State', 'options': list([ - 'Disconnected', - 'Connected', - 'Pending', - 'Firmware Mismatch', - 'Upgrading', - 'Provisioning', - 'Heartbeat Missed', - 'Adopting', - 'Deleting', - 'Inform Error', - 'Adoption Failed', - 'Isolated', - 'Unknown', + 'disconnected', + 'connected', + 'pending', + 'firmware_mismatch', + 'upgrading', + 'provisioning', + 'heartbeat_missed', + 'adopting', + 'deleting', + 'inform_error', + 'adoption_failed', + 'isolated', ]), }), 'context': , @@ -1669,7 +1694,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Connected', + 'state': 'connected', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_uptime-entry] @@ -1679,6 +1704,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1728,6 +1754,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1777,6 +1804,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1828,6 +1856,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1877,6 +1906,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1926,6 +1956,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1977,6 +2008,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2026,6 +2058,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/unifi/snapshots/test_switch.ambr b/tests/components/unifi/snapshots/test_switch.ambr index 45e6188a3f4..c07a4799b5a 100644 --- a/tests/components/unifi/snapshots/test_switch.ambr +++ b/tests/components/unifi/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -99,6 +101,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -146,6 +149,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -193,6 +197,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -240,6 +245,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -287,6 +293,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -334,6 +341,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -381,6 +389,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -428,6 +437,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -475,6 +485,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/unifi/snapshots/test_update.ambr b/tests/components/unifi/snapshots/test_update.ambr index 405cb9d52a6..ef3803ac53d 100644 --- a/tests/components/unifi/snapshots/test_update.ambr +++ b/tests/components/unifi/snapshots/test_update.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -124,6 +126,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -183,6 +186,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index fc3aeccea9f..94343d12ba2 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -21,7 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( ConfigEntryFactoryType, @@ -262,7 +262,7 @@ async def test_device_button_entities( WLAN_REGENERATE_PASSWORD, "button.ssid_1_regenerate_password", "put", - f"/rest/wlanconf/{WLAN_REGENERATE_PASSWORD[0]["_id"]}", + f"/rest/wlanconf/{WLAN_REGENERATE_PASSWORD[0]['_id']}", { "json": {"data": "password changed successfully", "meta": {"rc": "ok"}}, "headers": {"content-type": CONTENT_TYPE_JSON}, diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 71b196550da..9d85dedbc9a 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -7,7 +7,6 @@ import aiounifi import pytest from homeassistant import config_entries -from homeassistant.components import ssdp from homeassistant.components.unifi.config_flow import _async_discover_unifi from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -33,6 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from .conftest import ConfigEntryFactoryType @@ -482,7 +482,7 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://192.168.208.1:41417/rootDesc.xml", @@ -522,7 +522,7 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass: HomeAssistant) -> N result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/rootDesc.xml", @@ -544,7 +544,7 @@ async def test_form_ssdp_aborts_if_serial_already_exists(hass: HomeAssistant) -> result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/rootDesc.xml", @@ -570,7 +570,7 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass: HomeAssistant) -> No result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:1234/rootDesc.xml", diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index c653370656d..39b70344db7 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -26,7 +26,7 @@ from homeassistant.components.unifi.const import ( from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( ConfigEntryFactoryType, @@ -589,14 +589,14 @@ async def test_restoring_client( entity_registry.async_get_or_create( # Make sure unique ID converts to site_id-mac TRACKER_DOMAIN, UNIFI_DOMAIN, - f'{clients_all_payload[0]["mac"]}-site_id', + f"{clients_all_payload[0]['mac']}-site_id", suggested_object_id=clients_all_payload[0]["hostname"], config_entry=config_entry, ) entity_registry.async_get_or_create( # Unique ID already follow format site_id-mac TRACKER_DOMAIN, UNIFI_DOMAIN, - f'site_id-{client_payload[0]["mac"]}', + f"site_id-{client_payload[0]['mac']}", suggested_object_id=client_payload[0]["hostname"], config_entry=config_entry, ) diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index af134c7449b..5492f6fe0df 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ConfigEntryFactoryType, WebsocketStateManager diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 3c94d12018d..ee8b102edaa 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -30,13 +30,14 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, + STATE_UNKNOWN, EntityCategory, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryDisabler -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .conftest import ( ConfigEntryFactoryType, @@ -891,7 +892,9 @@ async def test_device_state( for i in list(map(int, DeviceState)): device["state"] = i mock_websocket_message(message=MessageKey.DEVICE, data=device) - assert hass.states.get("sensor.device_state").state == DEVICE_STATES[i] + assert hass.states.get("sensor.device_state").state == DEVICE_STATES.get( + i, STATE_UNKNOWN + ) @pytest.mark.parametrize( diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index ef93afa7e3e..e4765d1181e 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -809,6 +809,24 @@ TRAFFIC_RULE = { "target_devices": [{"client_mac": CLIENT_1["mac"], "type": "CLIENT"}], } +TRAFFIC_ROUTE = { + "_id": "676f8dbb8f1d54503bba19ab", + "description": "Test traffic route", + "domains": [{"domain": "youtube.com", "port_ranges": [], "ports": []}], + "enabled": True, + "ip_addresses": [], + "ip_ranges": [], + "kill_switch_enabled": True, + "matching_target": "DOMAIN", + "network_id": "676f8d288f1d54503bba1987", + "next_hop": "", + "regions": [], + "target_devices": [ + {"network_id": "6060b00f45de3905133cea14", "type": "NETWORK"}, + {"network_id": "6060ae6045de3905133cea0a", "type": "NETWORK"}, + ], +} + @pytest.mark.parametrize( "config_entry_options", [{CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}] @@ -1154,6 +1172,60 @@ async def test_traffic_rules( assert aioclient_mock.mock_calls[call_count][2] == expected_enable_call +@pytest.mark.parametrize(("traffic_route_payload"), [([TRAFFIC_ROUTE])]) +async def test_traffic_routes( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + config_entry_setup: MockConfigEntry, + traffic_route_payload: list[dict[str, Any]], +) -> None: + """Test control of UniFi traffic routes.""" + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + # Validate state object + assert hass.states.get("switch.unifi_network_test_traffic_route").state == STATE_ON + + traffic_route = deepcopy(traffic_route_payload[0]) + + # Disable traffic route + aioclient_mock.put( + f"https://{config_entry_setup.data[CONF_HOST]}:1234" + f"/v2/api/site/{config_entry_setup.data[CONF_SITE_ID]}" + f"/trafficroutes/{traffic_route['_id']}", + ) + + call_count = aioclient_mock.call_count + + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": "switch.unifi_network_test_traffic_route"}, + blocking=True, + ) + # Updating the value for traffic routes will make another call to retrieve the values + assert aioclient_mock.call_count == call_count + 2 + expected_disable_call = deepcopy(traffic_route) + expected_disable_call["enabled"] = False + + assert aioclient_mock.mock_calls[call_count][2] == expected_disable_call + + call_count = aioclient_mock.call_count + + # Enable traffic route + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_on", + {"entity_id": "switch.unifi_network_test_traffic_route"}, + blocking=True, + ) + + expected_enable_call = deepcopy(traffic_route) + expected_enable_call["enabled"] = True + + assert aioclient_mock.call_count == call_count + 2 + assert aioclient_mock.mock_calls[call_count][2] == expected_enable_call + + @pytest.mark.parametrize( ("device_payload", "entity_id", "outlet_index", "expected_switches"), [ @@ -1577,14 +1649,14 @@ async def test_updating_unique_id( entity_registry.async_get_or_create( SWITCH_DOMAIN, UNIFI_DOMAIN, - f'{device_payload[0]["mac"]}-outlet-1', + f"{device_payload[0]['mac']}-outlet-1", suggested_object_id="plug_outlet_1", config_entry=config_entry, ) entity_registry.async_get_or_create( SWITCH_DOMAIN, UNIFI_DOMAIN, - f'{device_payload[1]["mac"]}-poe-1', + f"{device_payload[1]['mac']}-poe-1", suggested_object_id="switch_port_1_poe", config_entry=config_entry, ) diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 3ed559b71ec..c49ade514bc 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -33,7 +33,7 @@ from uiprotect.websocket import WebsocketState from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from . import _patch_discovery from .utils import MockUFPFixture @@ -98,6 +98,7 @@ def bootstrap_fixture(nvr: NVR): data["events"] = [] data["doorlocks"] = [] data["chimes"] = [] + data["aiports"] = [] return Bootstrap.from_unifi_dict(**data) diff --git a/tests/components/unifiprotect/fixtures/sample_bootstrap.json b/tests/components/unifiprotect/fixtures/sample_bootstrap.json index 240a9938b64..4c8d86a787d 100644 --- a/tests/components/unifiprotect/fixtures/sample_bootstrap.json +++ b/tests/components/unifiprotect/fixtures/sample_bootstrap.json @@ -564,6 +564,24 @@ "legacyUFVs": [], "lastUpdateId": "ebf25bac-d5a1-4f1d-a0ee-74c15981eb70", "displays": [], + "ringtones": [ + { + "id": "66a14fa502d44203e40003eb", + "name": "Default", + "size": 208, + "isDefault": true, + "nvrMac": "A1E00C826924", + "modelKey": "ringtone" + }, + { + "id": "66a14fa502da4203e40003ec", + "name": "Traditional", + "size": 180, + "isDefault": false, + "nvrMac": "A1E00C826924", + "modelKey": "ringtone" + } + ], "bridges": [ { "mac": "A28D0DB15AE1", diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 12b92beedd0..975e93edf09 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -174,7 +174,7 @@ def validate_common_camera_state( entity_id: str, features: int = CameraEntityFeature.STREAM, ): - """Validate state that is common to all camera entity, regradless of type.""" + """Validate state that is common to all camera entity, regardless of type.""" entity_state = hass.states.get(entity_id) assert entity_state assert entity_state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 8bfdc004092..0eae2a48fea 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -11,7 +11,6 @@ from uiprotect import NotAuthorized, NvrError, ProtectApiClient from uiprotect.data import NVR, Bootstrap, CloudAccount from homeassistant import config_entries -from homeassistant.components import dhcp, ssdp from homeassistant.components.unifiprotect.const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, @@ -23,6 +22,8 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from . import ( DEVICE_HOSTNAME, @@ -37,13 +38,13 @@ from .conftest import MAC_ADDR from tests.common import MockConfigEntry -DHCP_DISCOVERY = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY = DhcpServiceInfo( hostname=DEVICE_HOSTNAME, ip=DEVICE_IP_ADDRESS, macaddress=DEVICE_MAC_ADDRESS.lower().replace(":", ""), ) SSDP_DISCOVERY = ( - ssdp.SsdpServiceInfo( + SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{DEVICE_IP_ADDRESS}:41417/rootDesc.xml", @@ -338,7 +339,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - ], ) async def test_discovered_by_ssdp_or_dhcp( - hass: HomeAssistant, source: str, data: dhcp.DhcpServiceInfo | ssdp.SsdpServiceInfo + hass: HomeAssistant, source: str, data: DhcpServiceInfo | SsdpServiceInfo ) -> None: """Test we handoff to unifi-discovery when discovered via ssdp or dhcp.""" diff --git a/tests/components/unifiprotect/test_event.py b/tests/components/unifiprotect/test_event.py index 6a26738f5e8..032a3b253a7 100644 --- a/tests/components/unifiprotect/test_event.py +++ b/tests/components/unifiprotect/test_event.py @@ -175,6 +175,10 @@ async def test_doorbell_nfc_scanned( Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[1] ) + ulp_id = "ulp_id" + test_user_full_name = "Test User" + test_nfc_id = "test_nfc_id" + unsub = async_track_state_change_event(hass, entity_id, _capture_event) event = Event( model=ModelType.EVENT, @@ -187,7 +191,224 @@ async def test_doorbell_nfc_scanned( smart_detect_event_ids=[], camera_id=doorbell.id, api=ufp.api, - metadata={"nfc": {"nfc_id": "test_nfc_id", "user_id": "test_user_id"}}, + metadata={"nfc": {"nfc_id": test_nfc_id, "user_id": "test_user_id"}}, + ) + + new_camera = doorbell.model_copy() + new_camera.last_nfc_card_scanned_event_id = "test_event_id" + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_keyring = Mock() + mock_keyring.registry_id = test_nfc_id + mock_keyring.registry_type = "nfc" + mock_keyring.ulp_user = ulp_id + ufp.api.bootstrap.keyrings.add(mock_keyring) + + mock_ulp_user = Mock() + mock_ulp_user.ulp_id = ulp_id + mock_ulp_user.full_name = test_user_full_name + mock_ulp_user.status = "ACTIVE" + ufp.api.bootstrap.ulp_users.add(mock_ulp_user) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + assert len(events) == 1 + state = events[0].data["new_state"] + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_ID] == "test_event_id" + assert state.attributes["nfc_id"] == "test_nfc_id" + assert state.attributes["full_name"] == test_user_full_name + + unsub() + + +async def test_doorbell_nfc_scanned_ulpusr_deactivated( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test a doorbell NFC scanned event.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 3, 3) + events: list[HAEvent] = [] + + @callback + def _capture_event(event: HAEvent) -> None: + events.append(event) + + _, entity_id = ids_from_device_description( + Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[1] + ) + + ulp_id = "ulp_id" + test_user_full_name = "Test User" + test_nfc_id = "test_nfc_id" + + unsub = async_track_state_change_event(hass, entity_id, _capture_event) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.NFC_CARD_SCANNED, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + metadata={"nfc": {"nfc_id": test_nfc_id, "user_id": "test_user_id"}}, + ) + + new_camera = doorbell.model_copy() + new_camera.last_nfc_card_scanned_event_id = "test_event_id" + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_keyring = Mock() + mock_keyring.registry_id = test_nfc_id + mock_keyring.registry_type = "nfc" + mock_keyring.ulp_user = ulp_id + ufp.api.bootstrap.keyrings.add(mock_keyring) + + mock_ulp_user = Mock() + mock_ulp_user.ulp_id = ulp_id + mock_ulp_user.full_name = test_user_full_name + mock_ulp_user.status = "DEACTIVATED" + ufp.api.bootstrap.ulp_users.add(mock_ulp_user) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + assert len(events) == 1 + state = events[0].data["new_state"] + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_ID] == "test_event_id" + assert state.attributes["nfc_id"] == "test_nfc_id" + assert state.attributes["full_name"] == "Test User" + assert state.attributes["user_status"] == "DEACTIVATED" + + unsub() + + +async def test_doorbell_nfc_scanned_no_ulpusr( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test a doorbell NFC scanned event.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 3, 3) + events: list[HAEvent] = [] + + @callback + def _capture_event(event: HAEvent) -> None: + events.append(event) + + _, entity_id = ids_from_device_description( + Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[1] + ) + + ulp_id = "ulp_id" + test_nfc_id = "test_nfc_id" + + unsub = async_track_state_change_event(hass, entity_id, _capture_event) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.NFC_CARD_SCANNED, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + metadata={"nfc": {"nfc_id": test_nfc_id, "user_id": "test_user_id"}}, + ) + + new_camera = doorbell.model_copy() + new_camera.last_nfc_card_scanned_event_id = "test_event_id" + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_keyring = Mock() + mock_keyring.registry_id = test_nfc_id + mock_keyring.registry_type = "nfc" + mock_keyring.ulp_user = ulp_id + ufp.api.bootstrap.keyrings.add(mock_keyring) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + assert len(events) == 1 + state = events[0].data["new_state"] + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_ID] == "test_event_id" + assert state.attributes["nfc_id"] == "test_nfc_id" + assert state.attributes["full_name"] == "" + + unsub() + + +async def test_doorbell_nfc_scanned_no_keyring( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test a doorbell NFC scanned event.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 3, 3) + events: list[HAEvent] = [] + + @callback + def _capture_event(event: HAEvent) -> None: + events.append(event) + + _, entity_id = ids_from_device_description( + Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[1] + ) + + test_nfc_id = "test_nfc_id" + + unsub = async_track_state_change_event(hass, entity_id, _capture_event) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.NFC_CARD_SCANNED, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + metadata={"nfc": {"nfc_id": test_nfc_id, "user_id": "test_user_id"}}, ) new_camera = doorbell.model_copy() @@ -208,6 +429,7 @@ async def test_doorbell_nfc_scanned( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_EVENT_ID] == "test_event_id" assert state.attributes["nfc_id"] == "test_nfc_id" + assert state.attributes["full_name"] == "" unsub() @@ -233,6 +455,9 @@ async def test_doorbell_fingerprint_identified( Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[2] ) + ulp_id = "ulp_id" + test_user_full_name = "Test User" + unsub = async_track_state_change_event(hass, entity_id, _capture_event) event = Event( model=ModelType.EVENT, @@ -245,7 +470,143 @@ async def test_doorbell_fingerprint_identified( smart_detect_event_ids=[], camera_id=doorbell.id, api=ufp.api, - metadata={"fingerprint": {"ulp_id": "test_ulp_id"}}, + metadata={"fingerprint": {"ulp_id": ulp_id}}, + ) + + new_camera = doorbell.model_copy() + new_camera.last_fingerprint_identified_event_id = "test_event_id" + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_ulp_user = Mock() + mock_ulp_user.ulp_id = ulp_id + mock_ulp_user.full_name = test_user_full_name + mock_ulp_user.status = "ACTIVE" + ufp.api.bootstrap.ulp_users.add(mock_ulp_user) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + assert len(events) == 1 + state = events[0].data["new_state"] + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_ID] == "test_event_id" + assert state.attributes["ulp_id"] == ulp_id + assert state.attributes["full_name"] == test_user_full_name + + unsub() + + +async def test_doorbell_fingerprint_identified_user_deactivated( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test a doorbell fingerprint identified event.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 3, 3) + events: list[HAEvent] = [] + + @callback + def _capture_event(event: HAEvent) -> None: + events.append(event) + + _, entity_id = ids_from_device_description( + Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[2] + ) + + ulp_id = "ulp_id" + test_user_full_name = "Test User" + + unsub = async_track_state_change_event(hass, entity_id, _capture_event) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.FINGERPRINT_IDENTIFIED, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + metadata={"fingerprint": {"ulp_id": ulp_id}}, + ) + + new_camera = doorbell.model_copy() + new_camera.last_fingerprint_identified_event_id = "test_event_id" + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + + mock_ulp_user = Mock() + mock_ulp_user.ulp_id = ulp_id + mock_ulp_user.full_name = test_user_full_name + mock_ulp_user.status = "DEACTIVATED" + ufp.api.bootstrap.ulp_users.add(mock_ulp_user) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = event + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() + + assert len(events) == 1 + state = events[0].data["new_state"] + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert state.attributes[ATTR_EVENT_ID] == "test_event_id" + assert state.attributes["ulp_id"] == ulp_id + assert state.attributes["full_name"] == "Test User" + assert state.attributes["user_status"] == "DEACTIVATED" + + unsub() + + +async def test_doorbell_fingerprint_identified_no_user( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, +) -> None: + """Test a doorbell fingerprint identified event.""" + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.EVENT, 3, 3) + events: list[HAEvent] = [] + + @callback + def _capture_event(event: HAEvent) -> None: + events.append(event) + + _, entity_id = ids_from_device_description( + Platform.EVENT, doorbell, EVENT_DESCRIPTIONS[2] + ) + + ulp_id = "ulp_id" + + unsub = async_track_state_change_event(hass, entity_id, _capture_event) + event = Event( + model=ModelType.EVENT, + id="test_event_id", + type=EventType.FINGERPRINT_IDENTIFIED, + start=fixed_now - timedelta(seconds=1), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + api=ufp.api, + metadata={"fingerprint": {"ulp_id": ulp_id}}, ) new_camera = doorbell.model_copy() @@ -265,7 +626,8 @@ async def test_doorbell_fingerprint_identified( assert state assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION assert state.attributes[ATTR_EVENT_ID] == "test_event_id" - assert state.attributes["ulp_id"] == "test_ulp_id" + assert state.attributes["ulp_id"] == ulp_id + assert state.attributes["full_name"] == "" unsub() diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 724ed108673..85de85cd1c1 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -95,42 +95,38 @@ async def test_light_update( async def test_light_turn_on( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light ) -> None: - """Test light entity turn off.""" + """Test light entity turn on.""" + + light._api = ufp.api + light.api.set_light_is_led_force_on = AsyncMock() await init_entry(hass, ufp, [light, unadopted_light]) assert_entity_counts(hass, Platform.LIGHT, 1, 1) entity_id = "light.test_light" - light.__pydantic_fields__["set_light"] = Mock(final=False, frozen=False) - light.set_light = AsyncMock() - await hass.services.async_call( - "light", - "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, - blocking=True, + "light", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - light.set_light.assert_called_once_with(True, 3) + assert light.api.set_light_is_led_force_on.called + assert light.api.set_light_is_led_force_on.call_args == ((light.id, True),) async def test_light_turn_off( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light ) -> None: - """Test light entity turn on.""" + """Test light entity turn off.""" + + light._api = ufp.api + light.api.set_light_is_led_force_on = AsyncMock() await init_entry(hass, ufp, [light, unadopted_light]) assert_entity_counts(hass, Platform.LIGHT, 1, 1) entity_id = "light.test_light" - light.__pydantic_fields__["set_light"] = Mock(final=False, frozen=False) - light.set_light = AsyncMock() - await hass.services.async_call( - "light", - "turn_off", - {ATTR_ENTITY_ID: entity_id}, - blocking=True, + "light", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - light.set_light.assert_called_once_with(False) + assert light.api.set_light_is_led_force_on.called + assert light.api.set_light_is_led_force_on.call_args == ((light.id, False),) diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 84e0e74a492..9697d1f11a4 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -9,9 +9,19 @@ from uiprotect.data import Camera, Chime, Color, Light, ModelType from uiprotect.data.devices import CameraZone from uiprotect.exceptions import BadRequest -from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN +from homeassistant.components.unifiprotect.const import ( + ATTR_MESSAGE, + DOMAIN, + KEYRINGS_KEY_TYPE, + KEYRINGS_KEY_TYPE_ID_FINGERPRINT, + KEYRINGS_KEY_TYPE_ID_NFC, + KEYRINGS_ULP_ID, + KEYRINGS_USER_FULL_NAME, + KEYRINGS_USER_STATUS, +) from homeassistant.components.unifiprotect.services import ( SERVICE_ADD_DOORBELL_TEXT, + SERVICE_GET_USER_KEYRING_INFO, SERVICE_REMOVE_DOORBELL_TEXT, SERVICE_REMOVE_PRIVACY_ZONE, SERVICE_SET_CHIME_PAIRED, @@ -249,3 +259,86 @@ async def test_remove_privacy_zone( ) ufp.api.update_device.assert_called() assert not doorbell.privacy_zones + + +@pytest.mark.asyncio +async def get_user_keyring_info( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ufp: MockUFPFixture, + doorbell: Camera, +) -> None: + """Test get_user_keyring_info service.""" + + ulp_user = Mock(full_name="Test User", status="active", ulp_id="user_ulp_id") + keyring = Mock( + registry_type="nfc", + registry_id="123456", + ulp_user="user_ulp_id", + ) + keyring_2 = Mock( + registry_type="fingerprint", + registry_id="2", + ulp_user="user_ulp_id", + ) + ufp.api.bootstrap.ulp_users.as_list = Mock(return_value=[ulp_user]) + ufp.api.bootstrap.keyrings.as_list = Mock(return_value=[keyring, keyring_2]) + + await init_entry(hass, ufp, [doorbell]) + + camera_entry = entity_registry.async_get("binary_sensor.test_camera_doorbell") + + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_USER_KEYRING_INFO, + {ATTR_DEVICE_ID: camera_entry.device_id}, + blocking=True, + return_response=True, + ) + + assert response == { + "users": [ + { + KEYRINGS_USER_FULL_NAME: "Test User", + "keys": [ + { + KEYRINGS_KEY_TYPE: "nfc", + KEYRINGS_KEY_TYPE_ID_NFC: "123456", + }, + { + KEYRINGS_KEY_TYPE_ID_FINGERPRINT: "2", + KEYRINGS_KEY_TYPE: "fingerprint", + }, + ], + KEYRINGS_USER_STATUS: "active", + KEYRINGS_ULP_ID: "user_ulp_id", + }, + ], + } + + +async def test_get_user_keyring_info_no_users( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ufp: MockUFPFixture, + doorbell: Camera, +) -> None: + """Test get_user_keyring_info service with no users.""" + + ufp.api.bootstrap.ulp_users.as_list = Mock(return_value=[]) + ufp.api.bootstrap.keyrings.as_list = Mock(return_value=[]) + + await init_entry(hass, ufp, [doorbell]) + + camera_entry = entity_registry.async_get("binary_sensor.test_camera_doorbell") + + with pytest.raises( + HomeAssistantError, match="No users found, please check Protect permissions." + ): + await hass.services.async_call( + DOMAIN, + SERVICE_GET_USER_KEYRING_INFO, + {ATTR_DEVICE_ID: camera_entry.device_id}, + blocking=True, + return_response=True, + ) diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index 0f1b7791680..f787089b83f 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -12,6 +12,7 @@ from uiprotect.exceptions import ClientError from homeassistant.components.unifiprotect.views import ( async_generate_event_video_url, async_generate_proxy_event_video_url, + async_generate_snapshot_url, async_generate_thumbnail_url, ) from homeassistant.core import HomeAssistant @@ -169,6 +170,231 @@ async def test_thumbnail_invalid_entry_entry_id( assert response.status == 404 +async def test_snapshot_bad_nvr_id( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot URL with bad NVR id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, fixed_now) + url = url.replace(ufp.api.bootstrap.nvr.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called() + + +async def test_snapshot_bad_camera_id( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot URL with bad camera id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, fixed_now) + url = url.replace(camera.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called() + + +async def test_snapshot_bad_camera_perms( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot URL with bad camera perms.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, fixed_now) + + ufp.api.bootstrap.auth_user.all_permissions = [] + ufp.api.bootstrap.auth_user._perm_cache = {} + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 403 + ufp.api.request.assert_not_called() + + +async def test_snapshot_bad_timestamp( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot URL with bad timestamp params.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, fixed_now) + url = url.replace(fixed_now.replace(microsecond=0).isoformat(), "bad_time") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.request.assert_not_called() + + +async def test_snapshot_client_error( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot triggers client error at API.""" + + ufp.api.get_camera_snapshot = AsyncMock(side_effect=ClientError()) + + tomorrow = fixed_now + timedelta(days=1) + + await init_entry(hass, ufp, [camera]) + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, tomorrow) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_camera_snapshot.assert_called_once() + + +async def test_snapshot_notfound( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot not found.""" + + ufp.api.get_camera_snapshot = AsyncMock(return_value=None) + + tomorrow = fixed_now + timedelta(days=1) + + await init_entry(hass, ufp, [camera]) + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, tomorrow) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_camera_snapshot.assert_called_once() + + +@pytest.mark.parametrize(("width", "height"), [("test", None), (None, "test")]) +async def test_snapshot_bad_params( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, + width: Any, + height: Any, +) -> None: + """Test invalid bad query parameters.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + url = async_generate_snapshot_url( + ufp.api.bootstrap.nvr.id, camera.id, fixed_now, width=width, height=height + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.request.assert_not_called() + + +async def test_snapshot( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test snapshot at timestamp in URL.""" + + ufp.api.get_camera_snapshot = AsyncMock(return_value=b"testtest") + await init_entry(hass, ufp, [camera]) + + # replace microseconds to match behavior of underlying library + fixed_now = fixed_now.replace(microsecond=0) + url = async_generate_snapshot_url(ufp.api.bootstrap.nvr.id, camera.id, fixed_now) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + # verify when height is None that it is called with camera high channel height + height = camera.high_camera_channel.height + + assert response.status == 200 + assert response.content_type == "image/jpeg" + assert await response.content.read() == b"testtest" + ufp.api.get_camera_snapshot.assert_called_once_with( + camera.id, None, height, dt=fixed_now + ) + + +@pytest.mark.parametrize(("width", "height"), [(123, None), (None, 456), (123, 456)]) +async def test_snapshot_with_dimensions( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, + width: Any, + height: Any, +) -> None: + """Test snapshot at timestamp in URL with specified width and height.""" + + ufp.api.get_camera_snapshot = AsyncMock(return_value=b"testtest") + await init_entry(hass, ufp, [camera]) + + # Replace microseconds to match behavior of underlying library + fixed_now = fixed_now.replace(microsecond=0) + url = async_generate_snapshot_url( + ufp.api.bootstrap.nvr.id, camera.id, fixed_now, width=width, height=height + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + # Assertions + assert response.status == 200 + assert response.content_type == "image/jpeg" + assert await response.content.read() == b"testtest" + ufp.api.get_camera_snapshot.assert_called_once_with( + camera.id, width, height, dt=fixed_now + ) + + async def test_video_bad_event( hass: HomeAssistant, ufp: MockUFPFixture, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 5a1ffa8258e..7dd0362f17c 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityDescription from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 5ebfd2c13ad..351e11db512 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -13,7 +13,7 @@ from homeassistant.components.media_player import ( MediaClass, MediaPlayerEntityFeature, ) -import homeassistant.components.universal.media_player as universal +from homeassistant.components.universal import media_player as universal from homeassistant.const import ( SERVICE_RELOAD, STATE_OFF, @@ -27,7 +27,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, get_fixture_path +from tests.common import MockEntityPlatform, async_mock_service, get_fixture_path CONFIG_CHILDREN_ONLY = { "name": "test", @@ -74,6 +74,7 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): self._shuffle = False self._sound_mode = None self._repeat = None + self.platform = MockEntityPlatform(hass) self.service_calls = { "turn_on": async_mock_service( @@ -361,26 +362,10 @@ async def test_config_bad_key(hass: HomeAssistant) -> None: async def test_platform_setup(hass: HomeAssistant) -> None: """Test platform setup.""" config = {"name": "test", "platform": "universal"} - bad_config = {"platform": "universal"} - entities = [] - - def add_entities(new_entities): - """Add devices to list.""" - entities.extend(new_entities) - - setup_ok = True - try: - await universal.async_setup_platform( - hass, validate_config(bad_config), add_entities - ) - except MultipleInvalid: - setup_ok = False - assert not setup_ok - assert len(entities) == 0 - - await universal.async_setup_platform(hass, validate_config(config), add_entities) - assert len(entities) == 1 - assert entities[0].name == "test" + assert await async_setup_component(hass, "media_player", {"media_player": config}) + await hass.async_block_till_done() + assert hass.states.async_all() != [] + assert hass.states.get("media_player.test") is not None async def test_master_state(hass: HomeAssistant) -> None: @@ -461,11 +446,10 @@ async def test_active_child_state(hass: HomeAssistant, mock_states) -> None: async def test_name(hass: HomeAssistant) -> None: """Test name property.""" - config = validate_config(CONFIG_CHILDREN_ONLY) - - ump = universal.UniversalMediaPlayer(hass, config) - - assert config["name"] == ump.name + assert await async_setup_component( + hass, "media_player", {"media_player": CONFIG_CHILDREN_ONLY} + ) + assert hass.states.get("media_player.test") is not None async def test_polling(hass: HomeAssistant) -> None: diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 59a4e97d22b..3909c7e5dc4 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -13,15 +13,20 @@ from homeassistant.data_entry_flow import FlowResultType def mocked_upb(sync_complete=True, config_ok=True): """Mock UPB lib.""" - def _upb_lib_connect(callback): + def _add_handler(_, callback): callback() + def _dummy_add_handler(_, _callback): + pass + upb_mock = AsyncMock() type(upb_mock).network_id = PropertyMock(return_value="42") type(upb_mock).config_ok = PropertyMock(return_value=config_ok) type(upb_mock).disconnect = MagicMock() - if sync_complete: - upb_mock.async_connect.side_effect = _upb_lib_connect + type(upb_mock).add_handler = MagicMock() + upb_mock.add_handler.side_effect = ( + _add_handler if sync_complete else _dummy_add_handler + ) return patch( "homeassistant.components.upb.config_flow.upb_lib.UpbPim", return_value=upb_mock ) diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 202b3d32509..55138430ca0 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON, EntityCatego from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import MockUpdateEntity diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index d4916de8039..f3eb3f9344c 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -43,7 +43,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component @@ -857,7 +857,7 @@ async def test_name(hass: HomeAssistant) -> None: async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test update platform via config entry.""" async_add_entities([entity1, entity2, entity3, entity4]) diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 4bee5c0e589..300d925b82b 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -7,7 +7,7 @@ import copy from datetime import datetime import socket from typing import Any -from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch +from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from urllib.parse import urlparse from async_upnp_client.aiohttp import AiohttpNotifyServer @@ -25,6 +25,15 @@ from homeassistant.components.upnp.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_DEVICE_TYPE, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_SERIAL, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -36,7 +45,7 @@ TEST_LOCATION6 = "http://[fe80::1%2]/desc.xml" TEST_HOST = urlparse(TEST_LOCATION).hostname TEST_FRIENDLY_NAME = "mock-name" TEST_MAC_ADDRESS = "00:11:22:33:44:55" -TEST_DISCOVERY = ssdp.SsdpServiceInfo( +TEST_DISCOVERY = SsdpServiceInfo( ssdp_st=TEST_ST, ssdp_udn=TEST_UDN, ssdp_usn=TEST_USN, @@ -45,12 +54,12 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( "_udn": TEST_UDN, "location": TEST_LOCATION, "usn": TEST_USN, - ssdp.ATTR_UPNP_DEVICE_TYPE: TEST_ST, - ssdp.ATTR_UPNP_FRIENDLY_NAME: TEST_FRIENDLY_NAME, - ssdp.ATTR_UPNP_MANUFACTURER: "mock-manufacturer", - ssdp.ATTR_UPNP_MODEL_NAME: "mock-model-name", - ssdp.ATTR_UPNP_SERIAL: "mock-serial", - ssdp.ATTR_UPNP_UDN: TEST_UDN, + ATTR_UPNP_DEVICE_TYPE: TEST_ST, + ATTR_UPNP_FRIENDLY_NAME: TEST_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER: "mock-manufacturer", + ATTR_UPNP_MODEL_NAME: "mock-model-name", + ATTR_UPNP_SERIAL: "mock-serial", + ATTR_UPNP_UDN: TEST_UDN, }, ssdp_headers={ "_host": TEST_HOST, @@ -75,13 +84,13 @@ def mock_igd_device(mock_async_create_device) -> IgdDevice: """Mock async_upnp_client device.""" mock_upnp_device = create_autospec(UpnpDevice, instance=True) mock_upnp_device.device_url = TEST_DISCOVERY.ssdp_location - mock_upnp_device.serial_number = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_SERIAL] + mock_upnp_device.serial_number = TEST_DISCOVERY.upnp[ATTR_UPNP_SERIAL] mock_igd_device = create_autospec(IgdDevice) mock_igd_device.device_type = TEST_DISCOVERY.ssdp_st - mock_igd_device.name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] - mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] - mock_igd_device.model_name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] + mock_igd_device.name = TEST_DISCOVERY.upnp[ATTR_UPNP_FRIENDLY_NAME] + mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ATTR_UPNP_MANUFACTURER] + mock_igd_device.model_name = TEST_DISCOVERY.upnp[ATTR_UPNP_MODEL_NAME] mock_igd_device.udn = TEST_DISCOVERY.ssdp_udn mock_igd_device.device = mock_upnp_device @@ -179,7 +188,7 @@ async def ssdp_instant_discovery(): async def register_callback( hass: HomeAssistant, callback: Callable[ - [ssdp.SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None + [SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None ], match_dict: dict[str, str] | None = None, ) -> MagicMock: @@ -212,7 +221,7 @@ async def ssdp_instant_discovery_multi_location(): async def register_callback( hass: HomeAssistant, callback: Callable[ - [ssdp.SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None + [SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None ], match_dict: dict[str, str] | None = None, ) -> MagicMock: @@ -241,7 +250,7 @@ async def ssdp_no_discovery(): async def register_callback( hass: HomeAssistant, callback: Callable[ - [ssdp.SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None + [SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None ], match_dict: dict[str, str] | None = None, ) -> MagicMock: @@ -286,11 +295,8 @@ async def mock_config_entry( # Load config_entry. entry.add_to_hass(hass) - with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py index 087cd9e9fb4..d9b5b442b00 100644 --- a/tests/components/upnp/test_binary_sensor.py +++ b/tests/components/upnp/test_binary_sensor.py @@ -6,7 +6,7 @@ from async_upnp_client.profiles.igd import IgdDevice, IgdState from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 8799e0faab3..fb650ac7a47 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_FORCE_POLL, CONFIG_ENTRY_HOST, @@ -21,6 +20,11 @@ from homeassistant.components.upnp.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_DEVICE_TYPE, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from .conftest import ( TEST_DISCOVERY, @@ -109,14 +113,14 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn=TEST_USN, # ssdp_udn=TEST_UDN, # Not provided. ssdp_st=TEST_ST, ssdp_location=TEST_LOCATION, upnp={ - ssdp.ATTR_UPNP_DEVICE_TYPE: ST_IGD_V1, - # ssdp.ATTR_UPNP_UDN: TEST_UDN, # Not provided. + ATTR_UPNP_DEVICE_TYPE: ST_IGD_V1, + # ATTR_UPNP_UDN: TEST_UDN, # Not provided. }, ), ) @@ -130,14 +134,14 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn=TEST_USN, ssdp_udn=TEST_UDN, ssdp_st=TEST_ST, ssdp_location=TEST_LOCATION, ssdp_all_locations=[TEST_LOCATION], upnp={ - ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:WFADevice:1", # Non-IGD + ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:WFADevice:1", # Non-IGD }, ), ) @@ -449,7 +453,7 @@ async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: """Test config flow: discovered + configured through ssdp, where the UDN differs in the SSDP-discovery vs device description.""" # Discovered via step ssdp. test_discovery = copy.deepcopy(TEST_DISCOVERY) - test_discovery.upnp[ssdp.ATTR_UPNP_UDN] = "uuid:another_udn" + test_discovery.upnp[ATTR_UPNP_UDN] = "uuid:another_udn" result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index ff74ca87b12..ef799a1b8af 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -22,6 +22,7 @@ from homeassistant.components.upnp.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceInfo from .conftest import ( TEST_DISCOVERY, @@ -125,7 +126,7 @@ async def test_async_setup_udn_mismatch( ) -> None: """Test async_setup_entry for a device which reports a different UDN from SSDP-discovery and device description.""" test_discovery = copy.deepcopy(TEST_DISCOVERY) - test_discovery.upnp[ssdp.ATTR_UPNP_UDN] = "uuid:another_udn" + test_discovery.upnp[ATTR_UPNP_UDN] = "uuid:another_udn" entry = MockConfigEntry( domain=DOMAIN, @@ -146,7 +147,7 @@ async def test_async_setup_udn_mismatch( async def register_callback( hass: HomeAssistant, callback: Callable[ - [ssdp.SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None + [SsdpServiceInfo, ssdp.SsdpChange], Coroutine[Any, Any, None] | None ], match_dict: dict[str, str] | None = None, ) -> MagicMock: diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index 67a64b265d9..e7461c91da4 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -3,14 +3,16 @@ from datetime import datetime, timedelta from async_upnp_client.profiles.igd import IgdDevice, IgdState +import pytest from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_upnp_sensors( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index 561e4b83320..d6d896dbcec 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -20,6 +20,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -49,6 +50,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index bbd802afc95..9730dba53d7 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -1,70 +1,44 @@ """Tests for the USB Discovery integration.""" +import asyncio +from datetime import timedelta +import logging import os -import sys +from typing import Any from unittest.mock import MagicMock, Mock, call, patch, sentinel +from aiousbwatcher import InotifyNotAvailableError import pytest from homeassistant.components import usb +from homeassistant.components.usb.utils import usb_device_from_port from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.usb import UsbServiceInfo from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import conbee_device, slae_sh_device +from tests.common import async_fire_time_changed, import_and_test_deprecated_constant from tests.typing import WebSocketGenerator -@pytest.fixture(name="operating_system") -def mock_operating_system(): - """Mock running Home Assistant Operating system.""" +@pytest.fixture(name="aiousbwatcher_no_inotify") +def aiousbwatcher_no_inotify(): + """Patch AIOUSBWatcher to not use inotify.""" with patch( - "homeassistant.components.usb.system_info.async_get_system_info", - return_value={ - "hassio": True, - "docker": True, - }, + "homeassistant.components.usb.AIOUSBWatcher.async_start", + side_effect=InotifyNotAvailableError, ): yield -@pytest.fixture(name="docker") -def mock_docker(): - """Mock running Home Assistant in docker container.""" - with patch( - "homeassistant.components.usb.system_info.async_get_system_info", - return_value={ - "hassio": False, - "docker": True, - }, - ): - yield - - -@pytest.fixture(name="venv") -def mock_venv(): - """Mock running Home Assistant in a venv container.""" - with patch( - "homeassistant.components.usb.system_info.async_get_system_info", - return_value={ - "hassio": False, - "docker": False, - "virtualenv": True, - }, - ): - yield - - -@pytest.mark.skipif( - not sys.platform.startswith("linux"), - reason="Only works on linux", -) -async def test_observer_discovery( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, venv +async def test_aiousbwatcher_discovery( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: - """Test that observer can discover a device without raising an exception.""" - new_usb = [{"domain": "test1", "vid": "3039"}] + """Test that aiousbwatcher can discover a device without raising an exception.""" + new_usb = [{"domain": "test1", "vid": "3039"}, {"domain": "test2", "vid": "0FA0"}] mock_comports = [ MagicMock( @@ -76,25 +50,23 @@ async def test_observer_discovery( description=slae_sh_device.description, ) ] - mock_observer = None - async def _mock_monitor_observer_callback(callback): - await hass.async_add_executor_job( - callback, MagicMock(action="create", device_path="/dev/new") - ) + aiousbwatcher_callback = None - def _create_mock_monitor_observer(monitor, callback, name): - nonlocal mock_observer - hass.create_task(_mock_monitor_observer_callback(callback)) - mock_observer = MagicMock() - return mock_observer + def async_register_callback(callback): + nonlocal aiousbwatcher_callback + aiousbwatcher_callback = callback + + MockAIOUSBWatcher = MagicMock() + MockAIOUSBWatcher.async_register_callback = async_register_callback with ( - patch("pyudev.Context"), - patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), - patch("pyudev.Monitor.filter_by"), + patch("sys.platform", "linux"), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch( + "homeassistant.components.usb.AIOUSBWatcher", return_value=MockAIOUSBWatcher + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -102,33 +74,98 @@ async def test_observer_discovery( hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + assert aiousbwatcher_callback is not None + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "test1" + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + + mock_comports.append( + MagicMock( + device=slae_sh_device.device, + vid=4000, + pid=4000, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + ) + + aiousbwatcher_callback() + await hass.async_block_till_done() + + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=usb.ADD_REMOVE_SCAN_COOLDOWN) + ) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[1][1][0] == "test2" + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +async def test_polling_discovery( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test that polling can discover a device without raising an exception.""" + new_usb = [{"domain": "test1", "vid": "3039"}] + mock_comports_found_device = asyncio.Event() + + def get_comports() -> list: + nonlocal mock_comports + + # Only "find" a device after a few invocations + if len(mock_comports.mock_calls) < 5: + return [] + + mock_comports_found_device.set() + return [ + MagicMock( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + ] + + with ( + patch("sys.platform", "linux"), + patch( + "homeassistant.components.usb.POLLING_MONITOR_SCAN_PERIOD", + timedelta(seconds=0.01), + ), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch( + "homeassistant.components.usb.comports", side_effect=get_comports + ) as mock_comports, + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + # Wait until a new device is discovered after a few polling attempts + assert len(mock_config_flow.mock_calls) == 0 + await mock_comports_found_device.wait() + await hass.async_block_till_done(wait_background_tasks=True) + assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "test1" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - # pylint:disable-next=unnecessary-dunder-call - assert mock_observer.mock_calls == [call.start(), call.__bool__(), call.stop()] - - -@pytest.mark.skipif( - not sys.platform.startswith("linux"), - reason="Only works on linux", -) -async def test_removal_by_observer_before_started( - hass: HomeAssistant, operating_system -) -> None: - """Test a device is removed by the observer before started.""" - - async def _mock_monitor_observer_callback(callback): - await hass.async_add_executor_job( - callback, MagicMock(action="remove", device_path="/dev/new") - ) - - def _create_mock_monitor_observer(monitor, callback, name): - hass.async_create_task(_mock_monitor_observer_callback(callback)) +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +async def test_removal_by_aiousbwatcher_before_started(hass: HomeAssistant) -> None: + """Test a device is removed by the aiousbwatcher before started.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] mock_comports = [ @@ -145,7 +182,6 @@ async def test_removal_by_observer_before_started( with ( patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), - patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) @@ -161,6 +197,7 @@ async def test_removal_by_observer_before_started( await hass.async_block_till_done() +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -179,7 +216,6 @@ async def test_discovered_by_websocket_scan( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -198,6 +234,7 @@ async def test_discovered_by_websocket_scan( assert mock_config_flow.mock_calls[0][1][0] == "test1" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_limited_by_description_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -218,7 +255,6 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -237,6 +273,7 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( assert mock_config_flow.mock_calls[0][1][0] == "test1" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_most_targeted_matcher_wins( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -258,7 +295,6 @@ async def test_most_targeted_matcher_wins( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -277,6 +313,7 @@ async def test_most_targeted_matcher_wins( assert mock_config_flow.mock_calls[0][1][0] == "more" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_rejected_by_description_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -297,7 +334,6 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -315,6 +351,7 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -340,7 +377,6 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -359,6 +395,7 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( assert mock_config_flow.mock_calls[0][1][0] == "test1" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -379,7 +416,6 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -397,6 +433,7 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -422,7 +459,6 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -441,6 +477,7 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( assert mock_config_flow.mock_calls[0][1][0] == "test1" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -466,7 +503,6 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -484,6 +520,7 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -504,7 +541,6 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -522,6 +558,7 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_match_vid_only( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -540,7 +577,6 @@ async def test_discovered_by_websocket_scan_match_vid_only( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -559,6 +595,7 @@ async def test_discovered_by_websocket_scan_match_vid_only( assert mock_config_flow.mock_calls[0][1][0] == "test1" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_scan_match_vid_wrong_pid( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -577,7 +614,6 @@ async def test_discovered_by_websocket_scan_match_vid_wrong_pid( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -595,6 +631,7 @@ async def test_discovered_by_websocket_scan_match_vid_wrong_pid( assert len(mock_config_flow.mock_calls) == 0 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_discovered_by_websocket_no_vid_pid( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -613,7 +650,6 @@ async def test_discovered_by_websocket_no_vid_pid( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -631,9 +667,9 @@ async def test_discovered_by_websocket_no_vid_pid( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.parametrize("exception_type", [ImportError, OSError]) +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_non_matching_discovered_by_scanner_after_started( - hass: HomeAssistant, exception_type, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: """Test a websocket scan that does not match.""" new_usb = [{"domain": "test1", "vid": "4444", "pid": "4444"}] @@ -650,7 +686,6 @@ async def test_non_matching_discovered_by_scanner_after_started( ] with ( - patch("pyudev.Context", side_effect=exception_type), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -668,14 +703,10 @@ async def test_non_matching_discovered_by_scanner_after_started( assert len(mock_config_flow.mock_calls) == 0 -@pytest.mark.skipif( - not sys.platform.startswith("linux"), - reason="Only works on linux", -) -async def test_observer_on_wsl_fallback_without_throwing_exception( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator, venv +async def test_aiousbwatcher_on_wsl_fallback_without_throwing_exception( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: - """Test that observer on WSL failure results in fallback to scanning without raising an exception.""" + """Test that aiousbwatcher on WSL failure results in fallback to scanning without raising an exception.""" new_usb = [{"domain": "test1", "vid": "3039"}] mock_comports = [ @@ -690,8 +721,6 @@ async def test_observer_on_wsl_fallback_without_throwing_exception( ] with ( - patch("pyudev.Context"), - patch("pyudev.Monitor.filter_by", side_effect=ValueError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -710,24 +739,8 @@ async def test_observer_on_wsl_fallback_without_throwing_exception( assert mock_config_flow.mock_calls[0][1][0] == "test1" -@pytest.mark.skipif( - not sys.platform.startswith("linux"), - reason="Only works on linux", -) -async def test_not_discovered_by_observer_before_started_on_docker( - hass: HomeAssistant, docker -) -> None: - """Test a device is not discovered since observer is not running on bare docker.""" - - async def _mock_monitor_observer_callback(callback): - await hass.async_add_executor_job( - callback, MagicMock(action="add", device_path="/dev/new") - ) - - def _create_mock_monitor_observer(monitor, callback, name): - hass.async_create_task(_mock_monitor_observer_callback(callback)) - return MagicMock() - +async def test_discovered_by_aiousbwatcher_before_started(hass: HomeAssistant) -> None: + """Test a device is discovered since aiousbwatcher is now running.""" new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] mock_comports = [ @@ -740,23 +753,45 @@ async def test_not_discovered_by_observer_before_started_on_docker( description=slae_sh_device.description, ) ] + initial_mock_comports = [] + aiousbwatcher_callback = None + + def async_register_callback(callback): + nonlocal aiousbwatcher_callback + aiousbwatcher_callback = callback + + MockAIOUSBWatcher = MagicMock() + MockAIOUSBWatcher.async_register_callback = async_register_callback with ( + patch("sys.platform", "linux"), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), - patch("homeassistant.components.usb.comports", return_value=mock_comports), - patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), + patch( + "homeassistant.components.usb.comports", return_value=initial_mock_comports + ), + patch( + "homeassistant.components.usb.AIOUSBWatcher", return_value=MockAIOUSBWatcher + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() - with ( - patch("homeassistant.components.usb.comports", return_value=[]), - patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, - ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(mock_config_flow.mock_calls) == 0 + assert len(mock_config_flow.mock_calls) == 0 + + initial_mock_comports.extend(mock_comports) + aiousbwatcher_callback() + await hass.async_block_till_done() + + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=usb.ADD_REMOVE_SCAN_COOLDOWN) + ) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(mock_config_flow.mock_calls) == 1 def test_get_serial_by_id_no_dir() -> None: @@ -839,6 +874,7 @@ def test_human_readable_device_name() -> None: assert "8A2A" in name +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_async_is_plugged_in( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -862,7 +898,6 @@ async def test_async_is_plugged_in( } with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(hass.config_entries.flow, "async_init"), @@ -885,6 +920,7 @@ async def test_async_is_plugged_in( assert usb.async_is_plugged_in(hass, matcher) +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") @pytest.mark.parametrize( "matcher", [ @@ -903,7 +939,6 @@ async def test_async_is_plugged_in_case_enforcement( new_usb = [{"domain": "test1", "vid": "ABCD"}] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(hass.config_entries.flow, "async_init"), @@ -917,6 +952,7 @@ async def test_async_is_plugged_in_case_enforcement( usb.async_is_plugged_in(hass, matcher) +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_web_socket_triggers_discovery_request_callbacks( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -924,7 +960,6 @@ async def test_web_socket_triggers_discovery_request_callbacks( mock_callback = Mock() with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=[]), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(hass.config_entries.flow, "async_init"), @@ -952,6 +987,7 @@ async def test_web_socket_triggers_discovery_request_callbacks( assert len(mock_callback.mock_calls) == 1 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_initial_scan_callback( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -960,7 +996,6 @@ async def test_initial_scan_callback( mock_callback_2 = Mock() with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=[]), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(hass.config_entries.flow, "async_init"), @@ -988,6 +1023,7 @@ async def test_initial_scan_callback( cancel_2() +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_cancel_initial_scan_callback( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -995,7 +1031,6 @@ async def test_cancel_initial_scan_callback( mock_callback = Mock() with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=[]), patch("homeassistant.components.usb.comports", return_value=[]), patch.object(hass.config_entries.flow, "async_init"), @@ -1014,6 +1049,7 @@ async def test_cancel_initial_scan_callback( assert len(mock_callback.mock_calls) == 0 +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") async def test_resolve_serial_by_id( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: @@ -1032,7 +1068,6 @@ async def test_resolve_serial_by_id( ] with ( - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=mock_comports), patch( @@ -1056,6 +1091,7 @@ async def test_resolve_serial_by_id( assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/serial/by-id/bla" +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") @pytest.mark.parametrize( "ports", [ @@ -1140,7 +1176,6 @@ async def test_cp2102n_ordering_on_macos( with ( patch("sys.platform", "darwin"), - patch("pyudev.Context", side_effect=ImportError), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch("homeassistant.components.usb.comports", return_value=ports), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, @@ -1160,3 +1195,192 @@ async def test_cp2102n_ordering_on_macos( # We always use `cu.SLAB_USBtoUART` assert mock_config_flow.mock_calls[0][2]["data"].device == "/dev/cu.SLAB_USBtoUART2" + + +@pytest.mark.parametrize( + ("constant_name", "replacement_name", "replacement"), + [ + ( + "UsbServiceInfo", + "homeassistant.helpers.service_info.usb.UsbServiceInfo", + UsbServiceInfo, + ), + ], +) +def test_deprecated_constants( + caplog: pytest.LogCaptureFixture, + constant_name: str, + replacement_name: str, + replacement: Any, +) -> None: + """Test deprecated automation constants.""" + import_and_test_deprecated_constant( + caplog, + usb, + constant_name, + replacement_name, + replacement, + "2026.2", + ) + + +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@patch("homeassistant.components.usb.REQUEST_SCAN_COOLDOWN", 0) +async def test_register_port_event_callback( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> None: + """Test the registration of a port event callback.""" + + port1 = Mock( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + + port2 = Mock( + device=conbee_device.device, + vid=12346, + pid=12346, + serial_number=conbee_device.serial_number, + manufacturer=conbee_device.manufacturer, + description=conbee_device.description, + ) + + port1_usb = usb_device_from_port(port1) + port2_usb = usb_device_from_port(port2) + + ws_client = await hass_ws_client(hass) + + mock_callback1 = Mock() + mock_callback2 = Mock() + + # Start off with no ports + with ( + patch("homeassistant.components.usb.comports", return_value=[]), + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + + _cancel1 = usb.async_register_port_event_callback(hass, mock_callback1) + cancel2 = usb.async_register_port_event_callback(hass, mock_callback2) + + assert mock_callback1.mock_calls == [] + assert mock_callback2.mock_calls == [] + + # Add two new ports + with patch("homeassistant.components.usb.comports", return_value=[port1, port2]): + await ws_client.send_json({"id": 1, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + + assert mock_callback1.mock_calls == [call({port1_usb, port2_usb}, set())] + assert mock_callback2.mock_calls == [call({port1_usb, port2_usb}, set())] + + # Cancel the second callback + cancel2() + cancel2() + + mock_callback1.reset_mock() + mock_callback2.reset_mock() + + # Remove port 2 + with patch("homeassistant.components.usb.comports", return_value=[port1]): + await ws_client.send_json({"id": 2, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert mock_callback1.mock_calls == [call(set(), {port2_usb})] + assert mock_callback2.mock_calls == [] # The second callback was unregistered + + mock_callback1.reset_mock() + mock_callback2.reset_mock() + + # Keep port 2 removed + with patch("homeassistant.components.usb.comports", return_value=[port1]): + await ws_client.send_json({"id": 3, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + # Nothing changed so no callback is called + assert mock_callback1.mock_calls == [] + assert mock_callback2.mock_calls == [] + + # Unplug one and plug in the other + with patch("homeassistant.components.usb.comports", return_value=[port2]): + await ws_client.send_json({"id": 4, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + assert mock_callback1.mock_calls == [call({port2_usb}, {port1_usb})] + assert mock_callback2.mock_calls == [] + + +@pytest.mark.usefixtures("aiousbwatcher_no_inotify") +@patch("homeassistant.components.usb.REQUEST_SCAN_COOLDOWN", 0) +async def test_register_port_event_callback_failure( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test port event callback failure handling.""" + + port1 = Mock( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + + port2 = Mock( + device=conbee_device.device, + vid=12346, + pid=12346, + serial_number=conbee_device.serial_number, + manufacturer=conbee_device.manufacturer, + description=conbee_device.description, + ) + + port1_usb = usb_device_from_port(port1) + port2_usb = usb_device_from_port(port2) + + ws_client = await hass_ws_client(hass) + + mock_callback1 = Mock(side_effect=RuntimeError("Failure 1")) + mock_callback2 = Mock(side_effect=RuntimeError("Failure 2")) + + # Start off with no ports + with ( + patch("homeassistant.components.usb.comports", return_value=[]), + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + + usb.async_register_port_event_callback(hass, mock_callback1) + usb.async_register_port_event_callback(hass, mock_callback2) + + assert mock_callback1.mock_calls == [] + assert mock_callback2.mock_calls == [] + + # Add two new ports + with ( + patch("homeassistant.components.usb.comports", return_value=[port1, port2]), + caplog.at_level(logging.ERROR, logger="homeassistant.components.usb"), + ): + await ws_client.send_json({"id": 1, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + + # Both were called even though they raised exceptions + assert mock_callback1.mock_calls == [call({port1_usb, port2_usb}, set())] + assert mock_callback2.mock_calls == [call({port1_usb, port2_usb}, set())] + + assert caplog.text.count("Error in USB port event callback") == 2 + assert "Failure 1" in caplog.text + assert "Failure 2" in caplog.text diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 40d19422ced..e412d53a0d0 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -35,7 +35,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 560566d7c49..4901e069aee 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -374,6 +374,7 @@ async def test_change_device_source( # Configure source entity 3 (without a device) source_config_entry_3 = MockConfigEntry() + source_config_entry_3.add_to_hass(hass) source_entity_3 = entity_registry.async_get_or_create( "sensor", "test", diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index cd549c77913..eba7cf913db 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -12,9 +12,11 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) +from homeassistant.components.utility_meter import ( + select as um_select, + sensor as um_sensor, +) from homeassistant.components.utility_meter.const import DOMAIN, SERVICE_RESET -import homeassistant.components.utility_meter.select as um_select -import homeassistant.components.utility_meter.sensor as um_sensor from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -26,7 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, mock_restore_cache diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 348afac57f7..c671969c5ac 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -44,7 +44,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index 7b9ae4a9ff3..46054b21324 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -110,6 +112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -161,6 +164,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -212,6 +216,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -263,6 +268,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -312,6 +318,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -396,6 +403,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -482,6 +490,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -533,6 +542,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -580,6 +590,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/vacuum/conftest.py b/tests/components/vacuum/conftest.py index 6e6639431d0..2c700daece0 100644 --- a/tests/components/vacuum/conftest.py +++ b/tests/components/vacuum/conftest.py @@ -9,7 +9,7 @@ from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN, VacuumEntit from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er, frame -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import MockVacuum @@ -87,7 +87,7 @@ async def setup_vacuum_platform_test_entity( async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test vacuum platform via config entry.""" async_add_entities([entity]) diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 3a0cbafb4a1..381cc1caa47 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index db6cd242f3f..8ae054b5646 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -33,6 +33,7 @@ from .common import async_start from tests.common import ( MockConfigEntry, MockEntity, + MockEntityPlatform, MockModule, help_test_all, import_and_test_deprecated_constant_enum, @@ -272,6 +273,44 @@ async def test_send_command(hass: HomeAssistant, config_flow_fixture: None) -> N assert "test" in strings +async def test_supported_features_compat(hass: HomeAssistant) -> None: + """Test StateVacuumEntity using deprecated feature constants features.""" + + features = ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + ) + + class _LegacyConstantsStateVacuum(StateVacuumEntity): + _attr_supported_features = int(features) + _attr_fan_speed_list = ["silent", "normal", "pet hair"] + + entity = _LegacyConstantsStateVacuum() + entity.hass = hass + entity.platform = MockEntityPlatform(hass) + assert isinstance(entity.supported_features, int) + assert entity.supported_features == int(features) + assert entity.supported_features_compat is ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + ) + assert entity.state_attributes == { + "battery_level": None, + "battery_icon": "mdi:battery-unknown", + "fan_speed": None, + } + assert entity.capability_attributes == { + "fan_speed_list": ["silent", "normal", "pet hair"] + } + assert entity._deprecated_supported_features_reported + + async def test_vacuum_not_log_deprecated_state_warning( hass: HomeAssistant, mock_vacuum_entity: MockVacuum, diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index d8eb38a3b9b..a26f88f6982 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -22,7 +22,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import ( MockConfigEntry, @@ -174,7 +174,7 @@ def mock_config_entry(hass: HomeAssistant) -> tuple[MockConfigEntry, list[ValveE async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test platform via config entry.""" async_add_entities(entities) diff --git a/tests/components/velbus/__init__.py b/tests/components/velbus/__init__.py index 68bd639f464..102c4a4e777 100644 --- a/tests/components/velbus/__init__.py +++ b/tests/components/velbus/__init__.py @@ -1 +1,13 @@ """Tests for the Velbus component.""" + +from homeassistant.components.velbus import VelbusConfigEntry +from homeassistant.core import HomeAssistant + + +async def init_integration( + hass: HomeAssistant, + config_entry: VelbusConfigEntry, +) -> None: + """Load the Velbus integration.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index 402acb821be..65418790280 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -1,12 +1,24 @@ """Fixtures for the Velbus tests.""" from collections.abc import Generator -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, patch import pytest +from velbusaio.channels import ( + Blind, + Button, + ButtonCounter, + Dimmer, + LightSensor, + Relay, + SelectedProgram, + SensorNumber, + Temperature, +) +from velbusaio.module import Module +from homeassistant.components.velbus import VelbusConfigEntry from homeassistant.components.velbus.const import DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant @@ -16,14 +28,301 @@ from tests.common import MockConfigEntry @pytest.fixture(name="controller") -def mock_controller() -> Generator[MagicMock]: +def mock_controller( + mock_button: AsyncMock, + mock_relay: AsyncMock, + mock_temperature: AsyncMock, + mock_select: AsyncMock, + mock_buttoncounter: AsyncMock, + mock_sensornumber: AsyncMock, + mock_lightsensor: AsyncMock, + mock_dimmer: AsyncMock, + mock_module_no_subdevices: AsyncMock, + mock_module_subdevices: AsyncMock, + mock_cover: AsyncMock, + mock_cover_no_position: AsyncMock, +) -> Generator[AsyncMock]: """Mock a successful velbus controller.""" - with patch("homeassistant.components.velbus.Velbus", autospec=True) as controller: + with ( + patch("homeassistant.components.velbus.Velbus", autospec=True) as controller, + patch( + "homeassistant.components.velbus.config_flow.velbusaio.controller.Velbus", + new=controller, + ), + ): + cont = controller.return_value + cont.get_all_binary_sensor.return_value = [mock_button] + cont.get_all_button.return_value = [mock_button] + cont.get_all_switch.return_value = [mock_relay] + cont.get_all_climate.return_value = [mock_temperature] + cont.get_all_select.return_value = [mock_select] + cont.get_all_sensor.return_value = [ + mock_buttoncounter, + mock_temperature, + mock_sensornumber, + mock_lightsensor, + ] + cont.get_all_light.return_value = [mock_dimmer] + cont.get_all_led.return_value = [mock_button] + cont.get_all_cover.return_value = [mock_cover, mock_cover_no_position] + cont.get_modules.return_value = { + 1: mock_module_no_subdevices, + 2: mock_module_no_subdevices, + 3: mock_module_no_subdevices, + 4: mock_module_no_subdevices, + 99: mock_module_subdevices, + } + cont.get_module.return_value = mock_module_subdevices yield controller +@pytest.fixture +def mock_module_no_subdevices( + mock_relay: AsyncMock, +) -> AsyncMock: + """Mock a velbus module.""" + module = AsyncMock(spec=Module) + module.get_type_name.return_value = "VMB4RYLD" + module.get_addresses.return_value = [1, 2, 3, 4] + module.get_name.return_value = "BedRoom" + module.get_sw_version.return_value = "1.0.0" + module.is_loaded.return_value = True + module.get_channels.return_value = {} + return module + + +@pytest.fixture +def mock_module_subdevices() -> AsyncMock: + """Mock a velbus module.""" + module = AsyncMock(spec=Module) + module.get_type_name.return_value = "VMB2BLE" + module.get_addresses.return_value = [88] + module.get_name.return_value = "Kitchen" + module.get_sw_version.return_value = "2.0.0" + module.is_loaded.return_value = True + module.get_channels.return_value = {} + return module + + +@pytest.fixture +def mock_button() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=Button) + channel.get_categories.return_value = ["binary_sensor", "led", "button"] + channel.get_name.return_value = "ButtonOn" + channel.get_module_address.return_value = 1 + channel.get_channel_number.return_value = 1 + channel.get_module_type_name.return_value = "VMB4RYLD" + channel.get_module_type.return_value = 99 + channel.get_full_name.return_value = "Bedroom kid 1" + channel.get_module_sw_version.return_value = "1.0.0" + channel.get_module_serial.return_value = "a1b2c3d4e5f6" + channel.is_sub_device.return_value = False + channel.is_closed.return_value = True + channel.is_on.return_value = False + return channel + + +@pytest.fixture +def mock_temperature() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=Temperature) + channel.get_categories.return_value = ["sensor", "climate"] + channel.get_name.return_value = "Temperature" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 3 + channel.get_module_type_name.return_value = "VMB4GPO" + channel.get_full_name.return_value = "Living room" + channel.get_module_sw_version.return_value = "3.0.0" + channel.get_module_serial.return_value = "asdfghjk" + channel.get_module_type.return_value = 1 + channel.is_sub_device.return_value = False + channel.is_counter_channel.return_value = False + channel.get_class.return_value = "temperature" + channel.get_unit.return_value = "°C" + channel.get_state.return_value = 20.0 + channel.is_temperature.return_value = True + channel.get_max.return_value = 30.0 + channel.get_min.return_value = 10.0 + channel.get_climate_target.return_value = 21.0 + channel.get_climate_preset.return_value = 1 + channel.get_climate_mode.return_value = "day" + channel.get_cool_mode.return_value = "heat" + return channel + + +@pytest.fixture +def mock_relay() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=Relay) + channel.get_categories.return_value = ["switch"] + channel.get_name.return_value = "RelayName" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 55 + channel.get_module_type_name.return_value = "VMB4RYNO" + channel.get_full_name.return_value = "Living room" + channel.get_module_sw_version.return_value = "1.0.1" + channel.get_module_serial.return_value = "qwerty123" + channel.get_module_type.return_value = 2 + channel.is_sub_device.return_value = True + channel.is_on.return_value = True + return channel + + +@pytest.fixture +def mock_select() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=SelectedProgram) + channel.get_categories.return_value = ["select"] + channel.get_name.return_value = "select" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 33 + channel.get_module_type_name.return_value = "VMB4RYNO" + channel.get_module_type.return_value = 3 + channel.get_full_name.return_value = "Kitchen" + channel.get_module_sw_version.return_value = "1.1.1" + channel.get_module_serial.return_value = "qwerty1234567" + channel.is_sub_device.return_value = False + channel.get_options.return_value = ["none", "summer", "winter", "holiday"] + channel.get_selected_program.return_value = "winter" + return channel + + +@pytest.fixture +def mock_buttoncounter() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=ButtonCounter) + channel.get_categories.return_value = ["sensor"] + channel.get_name.return_value = "ButtonCounter" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 2 + channel.get_module_type_name.return_value = "VMB7IN" + channel.get_module_type.return_value = 4 + channel.get_full_name.return_value = "Input" + channel.get_module_sw_version.return_value = "1.0.0" + channel.get_module_serial.return_value = "a1b2c3d4e5f6" + channel.is_sub_device.return_value = True + channel.is_counter_channel.return_value = True + channel.is_temperature.return_value = False + channel.get_state.return_value = 100 + channel.get_unit.return_value = "W" + channel.get_counter_state.return_value = 100 + channel.get_counter_unit.return_value = "kWh" + return channel + + +@pytest.fixture +def mock_sensornumber() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=SensorNumber) + channel.get_categories.return_value = ["sensor"] + channel.get_name.return_value = "SensorNumber" + channel.get_module_address.return_value = 2 + channel.get_channel_number.return_value = 3 + channel.get_module_type_name.return_value = "VMB7IN" + channel.get_module_type.return_value = 8 + channel.get_full_name.return_value = "Input" + channel.get_module_sw_version.return_value = "1.0.0" + channel.get_module_serial.return_value = "a1b2c3d4e5f6" + channel.is_sub_device.return_value = False + channel.is_counter_channel.return_value = False + channel.is_temperature.return_value = False + channel.get_unit.return_value = "m" + channel.get_state.return_value = 10 + return channel + + +@pytest.fixture +def mock_lightsensor() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=LightSensor) + channel.get_categories.return_value = ["sensor"] + channel.get_name.return_value = "LightSensor" + channel.get_module_address.return_value = 2 + channel.get_channel_number.return_value = 4 + channel.get_module_type_name.return_value = "VMB7IN" + channel.get_module_type.return_value = 8 + channel.get_full_name.return_value = "Input" + channel.get_module_sw_version.return_value = "1.0.0" + channel.get_module_serial.return_value = "a1b2c3d4e5f6" + channel.is_sub_device.return_value = False + channel.is_counter_channel.return_value = False + channel.is_temperature.return_value = False + channel.get_unit.return_value = "illuminance" + channel.get_state.return_value = 250 + return channel + + +@pytest.fixture +def mock_dimmer() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=Dimmer) + channel.get_categories.return_value = ["light"] + channel.get_name.return_value = "Dimmer" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 10 + channel.get_module_type_name.return_value = "VMBDN1" + channel.get_module_type.return_value = 9 + channel.get_full_name.return_value = "Dimmer full name" + channel.get_module_sw_version.return_value = "1.0.0" + channel.get_module_serial.return_value = "a1b2c3d4e5f6g7" + channel.is_sub_device.return_value = True + channel.is_on.return_value = False + channel.get_dimmer_state.return_value = 33 + return channel + + +@pytest.fixture +def mock_cover() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=Blind) + channel.get_categories.return_value = ["cover"] + channel.get_name.return_value = "CoverName" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 9 + channel.get_module_type_name.return_value = "VMB2BLE" + channel.get_module_type.return_value = 10 + channel.get_full_name.return_value = "Basement" + channel.get_module_sw_version.return_value = "1.0.1" + channel.get_module_serial.return_value = "1234" + channel.is_sub_device.return_value = True + channel.support_position.return_value = True + channel.get_position.return_value = 50 + channel.is_closed.return_value = False + channel.is_open.return_value = True + channel.is_opening.return_value = False + channel.is_closing.return_value = False + return channel + + +@pytest.fixture +def mock_cover_no_position() -> AsyncMock: + """Mock a successful velbus channel.""" + channel = AsyncMock(spec=Blind) + channel.get_categories.return_value = ["cover"] + channel.get_name.return_value = "CoverNameNoPos" + channel.get_module_address.return_value = 88 + channel.get_channel_number.return_value = 11 + channel.get_module_type_name.return_value = "VMB2BLE" + channel.get_module_type.return_value = 10 + channel.get_full_name.return_value = "Basement" + channel.get_module_sw_version.return_value = "1.0.1" + channel.get_module_serial.return_value = "12345" + channel.is_sub_device.return_value = True + channel.support_position.return_value = False + channel.get_position.return_value = None + channel.is_closed.return_value = False + channel.is_open.return_value = True + channel.is_opening.return_value = True + channel.is_closing.return_value = True + return channel + + @pytest.fixture(name="config_entry") -def mock_config_entry(hass: HomeAssistant) -> ConfigEntry: +async def mock_config_entry( + hass: HomeAssistant, + controller: AsyncMock, +) -> VelbusConfigEntry: """Create and register mock config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/velbus/snapshots/test_binary_sensor.ambr b/tests/components/velbus/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..70db53257a1 --- /dev/null +++ b/tests/components/velbus/snapshots/test_binary_sensor.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_entities[binary_sensor.bedroom_kid_1_buttonon-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.bedroom_kid_1_buttonon', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ButtonOn', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[binary_sensor.bedroom_kid_1_buttonon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom kid 1 ButtonOn', + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom_kid_1_buttonon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_button.ambr b/tests/components/velbus/snapshots/test_button.ambr new file mode 100644 index 00000000000..856ebdb1e21 --- /dev/null +++ b/tests/components/velbus/snapshots/test_button.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_entities[button.bedroom_kid_1_buttonon-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.bedroom_kid_1_buttonon', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'ButtonOn', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[button.bedroom_kid_1_buttonon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom kid 1 ButtonOn', + }), + 'context': , + 'entity_id': 'button.bedroom_kid_1_buttonon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_climate.ambr b/tests/components/velbus/snapshots/test_climate.ambr new file mode 100644 index 00000000000..1d1f49d14d9 --- /dev/null +++ b/tests/components/velbus/snapshots/test_climate.ambr @@ -0,0 +1,77 @@ +# serializer version: 1 +# name: test_entities[climate.living_room_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'preset_modes': list([ + 'eco', + 'away', + 'home', + 'comfort', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.living_room_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'asdfghjk-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[climate.living_room_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 20.0, + 'friendly_name': 'Living room Temperature', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'preset_mode': None, + 'preset_modes': list([ + 'eco', + 'away', + 'home', + 'comfort', + ]), + 'supported_features': , + 'temperature': 21.0, + }), + 'context': , + 'entity_id': 'climate.living_room_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_cover.ambr b/tests/components/velbus/snapshots/test_cover.ambr new file mode 100644 index 00000000000..0be18034bc0 --- /dev/null +++ b/tests/components/velbus/snapshots/test_cover.ambr @@ -0,0 +1,99 @@ +# serializer version: 1 +# name: test_entities[cover.basement_covername-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.basement_covername', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CoverName', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '1234-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[cover.basement_covername-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 50, + 'friendly_name': 'Basement CoverName', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.basement_covername', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_entities[cover.basement_covernamenopos-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.basement_covernamenopos', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CoverNameNoPos', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '12345-11', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[cover.basement_covernamenopos-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'friendly_name': 'Basement CoverNameNoPos', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.basement_covernamenopos', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'opening', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_diagnostics.ambr b/tests/components/velbus/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c8bff1841e8 --- /dev/null +++ b/tests/components/velbus/snapshots/test_diagnostics.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'entry': dict({ + 'data': dict({ + 'name': 'velbus home', + 'port': '127.0.1.0.1:3788', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'velbus', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'Mock Title', + 'unique_id': None, + 'version': 2, + }), + 'modules': list([ + dict({ + 'address': list([ + 1, + 2, + 3, + 4, + ]), + 'channels': dict({ + }), + 'is_loaded': True, + 'name': 'BedRoom', + 'sw_version': '1.0.0', + 'type': 'VMB4RYLD', + }), + dict({ + 'address': list([ + 1, + 2, + 3, + 4, + ]), + 'channels': dict({ + }), + 'is_loaded': True, + 'name': 'BedRoom', + 'sw_version': '1.0.0', + 'type': 'VMB4RYLD', + }), + dict({ + 'address': list([ + 1, + 2, + 3, + 4, + ]), + 'channels': dict({ + }), + 'is_loaded': True, + 'name': 'BedRoom', + 'sw_version': '1.0.0', + 'type': 'VMB4RYLD', + }), + dict({ + 'address': list([ + 1, + 2, + 3, + 4, + ]), + 'channels': dict({ + }), + 'is_loaded': True, + 'name': 'BedRoom', + 'sw_version': '1.0.0', + 'type': 'VMB4RYLD', + }), + dict({ + 'address': list([ + 88, + ]), + 'channels': dict({ + }), + 'is_loaded': True, + 'name': 'Kitchen', + 'sw_version': '2.0.0', + 'type': 'VMB2BLE', + }), + ]), + }) +# --- diff --git a/tests/components/velbus/snapshots/test_init.ambr b/tests/components/velbus/snapshots/test_init.ambr new file mode 100644 index 00000000000..1e17753a02f --- /dev/null +++ b/tests/components/velbus/snapshots/test_init.ambr @@ -0,0 +1,253 @@ +# serializer version: 1 +# name: test_device_registry + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '1', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB4RYLD', + 'model_id': '99', + 'name': 'Bedroom kid 1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6', + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-9', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '1234', + 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-11', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB2BLE', + 'model_id': '10', + 'name': 'Basement', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '12345', + 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-10', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMBDN1', + 'model_id': '9', + 'name': 'Dimmer full name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6g7', + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-2', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB7IN', + 'model_id': '4', + 'name': 'Input', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6', + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': , + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB4GPO', + 'model_id': '1', + 'name': 'Living room', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'asdfghjk', + 'suggested_area': None, + 'sw_version': '3.0.0', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '2', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB7IN', + 'model_id': '8', + 'name': 'Input', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'a1b2c3d4e5f6', + 'suggested_area': None, + 'sw_version': '1.0.0', + 'via_device_id': None, + }), + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'velbus', + '88-55', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Velleman', + 'model': 'VMB4RYNO', + 'model_id': '2', + 'name': 'Living room', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'qwerty123', + 'suggested_area': None, + 'sw_version': '1.0.1', + 'via_device_id': , + }), + ]) +# --- diff --git a/tests/components/velbus/snapshots/test_light.ambr b/tests/components/velbus/snapshots/test_light.ambr new file mode 100644 index 00000000000..6dd2ca4939d --- /dev/null +++ b/tests/components/velbus/snapshots/test_light.ambr @@ -0,0 +1,114 @@ +# serializer version: 1 +# name: test_entities[light.bedroom_kid_1_led_buttonon-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': , + 'entity_id': 'light.bedroom_kid_1_led_buttonon', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LED ButtonOn', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[light.bedroom_kid_1_led_buttonon-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Bedroom kid 1 LED ButtonOn', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.bedroom_kid_1_led_buttonon', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[light.dimmer_full_name_dimmer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.dimmer_full_name_dimmer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dimmer', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6g7-10', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[light.dimmer_full_name_dimmer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Dimmer full name Dimmer', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.dimmer_full_name_dimmer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_select.ambr b/tests/components/velbus/snapshots/test_select.ambr new file mode 100644 index 00000000000..94bb109fc71 --- /dev/null +++ b/tests/components/velbus/snapshots/test_select.ambr @@ -0,0 +1,61 @@ +# serializer version: 1 +# name: test_entities[select.kitchen_select-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'summer', + 'winter', + 'holiday', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.kitchen_select', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'select', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'qwerty1234567-33-program_select', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[select.kitchen_select-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen select', + 'options': list([ + 'none', + 'summer', + 'winter', + 'holiday', + ]), + }), + 'context': , + 'entity_id': 'select.kitchen_select', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'winter', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_sensor.ambr b/tests/components/velbus/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..6f562f399af --- /dev/null +++ b/tests/components/velbus/snapshots/test_sensor.ambr @@ -0,0 +1,260 @@ +# serializer version: 1 +# name: test_entities[sensor.input_buttoncounter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.input_buttoncounter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ButtonCounter', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-2', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_entities[sensor.input_buttoncounter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Input ButtonCounter', + 'state_class': , + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.input_buttoncounter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_entities[sensor.input_buttoncounter_counter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.input_buttoncounter_counter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:counter', + 'original_name': 'ButtonCounter-counter', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-2-counter', + 'unit_of_measurement': 'kWh', + }) +# --- +# name: test_entities[sensor.input_buttoncounter_counter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Input ButtonCounter-counter', + 'icon': 'mdi:counter', + 'state_class': , + 'unit_of_measurement': 'kWh', + }), + 'context': , + 'entity_id': 'sensor.input_buttoncounter_counter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_entities[sensor.input_lightsensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.input_lightsensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LightSensor', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-4', + 'unit_of_measurement': 'illuminance', + }) +# --- +# name: test_entities[sensor.input_lightsensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Input LightSensor', + 'state_class': , + 'unit_of_measurement': 'illuminance', + }), + 'context': , + 'entity_id': 'sensor.input_lightsensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '250.0', + }) +# --- +# name: test_entities[sensor.input_sensornumber-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.input_sensornumber', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'SensorNumber', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a1b2c3d4e5f6-3', + 'unit_of_measurement': 'm', + }) +# --- +# name: test_entities[sensor.input_sensornumber-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Input SensorNumber', + 'state_class': , + 'unit_of_measurement': 'm', + }), + 'context': , + 'entity_id': 'sensor.input_sensornumber', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_entities[sensor.living_room_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.living_room_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'asdfghjk-3', + 'unit_of_measurement': , + }) +# --- +# name: test_entities[sensor.living_room_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Living room Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.living_room_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.0', + }) +# --- diff --git a/tests/components/velbus/snapshots/test_switch.ambr b/tests/components/velbus/snapshots/test_switch.ambr new file mode 100644 index 00000000000..60458b196a8 --- /dev/null +++ b/tests/components/velbus/snapshots/test_switch.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_entities[switch.living_room_relayname-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.living_room_relayname', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'RelayName', + 'platform': 'velbus', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'qwerty123-55', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.living_room_relayname-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Living room RelayName', + }), + 'context': , + 'entity_id': 'switch.living_room_relayname', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/velbus/test_binary_sensor.py b/tests/components/velbus/test_binary_sensor.py new file mode 100644 index 00000000000..29497cd36e1 --- /dev/null +++ b/tests/components/velbus/test_binary_sensor.py @@ -0,0 +1,26 @@ +"""Velbus binary_sensor platform tests.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.BINARY_SENSOR]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/velbus/test_button.py b/tests/components/velbus/test_button.py new file mode 100644 index 00000000000..cf334a29b05 --- /dev/null +++ b/tests/components/velbus/test_button.py @@ -0,0 +1,46 @@ +"""Velbus button platform tests.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.BUTTON]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_press( + hass: HomeAssistant, + mock_button: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test button press.""" + await init_integration(hass, config_entry) + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.bedroom_kid_1_buttonon"}, + blocking=True, + ) + mock_button.press.assert_called_once_with() diff --git a/tests/components/velbus/test_climate.py b/tests/components/velbus/test_climate.py new file mode 100644 index 00000000000..c843bca6e68 --- /dev/null +++ b/tests/components/velbus/test_climate.py @@ -0,0 +1,125 @@ +"""Velbus climate platform tests.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.climate import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + DOMAIN as CLIMATE_DOMAIN, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.CLIMATE]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +async def test_set_target_temperature( + hass: HomeAssistant, + mock_temperature: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test the target temperature climate action.""" + await init_integration(hass, config_entry) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.living_room_temperature", ATTR_TEMPERATURE: 29}, + blocking=True, + ) + mock_temperature.set_temp.assert_called_once_with(29) + + +@pytest.mark.parametrize( + ("set_mode", "expected_mode"), + [ + (PRESET_AWAY, "night"), + (PRESET_COMFORT, "comfort"), + (PRESET_ECO, "safe"), + (PRESET_HOME, "day"), + ], +) +async def test_set_preset_mode( + hass: HomeAssistant, + mock_temperature: AsyncMock, + config_entry: MockConfigEntry, + set_mode: str, + expected_mode: str, +) -> None: + """Test the preset mode climate action.""" + await init_integration(hass, config_entry) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.living_room_temperature", ATTR_PRESET_MODE: set_mode}, + blocking=True, + ) + mock_temperature.set_preset.assert_called_once_with(expected_mode) + + +@pytest.mark.parametrize( + ("set_mode"), + [ + ("heat"), + ("cool"), + ], +) +async def test_set_hvac_mode( + hass: HomeAssistant, + mock_temperature: AsyncMock, + config_entry: MockConfigEntry, + set_mode: str, +) -> None: + """Test the hvac mode climate action.""" + await init_integration(hass, config_entry) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room_temperature", ATTR_HVAC_MODE: set_mode}, + blocking=True, + ) + mock_temperature.set_mode.assert_called_once_with(set_mode) + + +async def test_set_hvac_mode_invalid( + hass: HomeAssistant, + mock_temperature: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test the hvac mode climate action with an invalid mode.""" + await init_integration(hass, config_entry) + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room_temperature", ATTR_HVAC_MODE: "auto"}, + blocking=True, + ) + mock_temperature.set_mode.assert_not_called() diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 5e81a3f8a36..04b6a51043f 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -7,18 +7,18 @@ import pytest import serial.tools.list_ports from velbusaio.exceptions import VelbusConnectionFailed -from homeassistant.components import usb from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import SOURCE_USB, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.usb import UsbServiceInfo from .const import PORT_SERIAL, PORT_TCP from tests.common import MockConfigEntry -DISCOVERY_INFO = usb.UsbServiceInfo( +DISCOVERY_INFO = UsbServiceInfo( device=PORT_SERIAL, pid="10CF", vid="0B1B", diff --git a/tests/components/velbus/test_cover.py b/tests/components/velbus/test_cover.py new file mode 100644 index 00000000000..24a90e0f8d1 --- /dev/null +++ b/tests/components/velbus/test_cover.py @@ -0,0 +1,90 @@ +"""Velbus cover platform tests.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.cover import ATTR_POSITION, DOMAIN as COVER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.COVER]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_id", "entity_num"), + [ + ("cover.basement_covername", 0), + ("cover.basement_covernamenopos", 1), + ], +) +async def test_actions( + hass: HomeAssistant, + config_entry: MockConfigEntry, + entity_id: str, + entity_num: int, +) -> None: + """Test the cover actions.""" + await init_integration(hass, config_entry) + entity = config_entry.runtime_data.controller.get_all_cover()[entity_num] + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + entity.close.assert_called_once() + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + entity.open.assert_called_once() + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + entity.stop.assert_called_once() + + +async def test_position( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_cover: AsyncMock, +) -> None: + """Test the set_postion over action.""" + await init_integration(hass, config_entry) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.basement_covername", ATTR_POSITION: 25}, + blocking=True, + ) + mock_cover.set_position.assert_called_once_with(75) diff --git a/tests/components/velbus/test_diagnostics.py b/tests/components/velbus/test_diagnostics.py new file mode 100644 index 00000000000..af84115ff14 --- /dev/null +++ b/tests/components/velbus/test_diagnostics.py @@ -0,0 +1,28 @@ +"""Test Velbus diagnostics.""" + +import pytest +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + await init_integration(hass, config_entry) + + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + assert result == snapshot(exclude=props("created_at", "modified_at", "entry_id")) diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index dea246b8a86..3285099f2a2 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -1,20 +1,55 @@ """Tests for the Velbus component initialisation.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from syrupy.assertion import SnapshotAssertion +from velbusaio.exceptions import VelbusConnectionFailed +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.velbus import VelbusConfigEntry from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, CONF_PORT, SERVICE_TURN_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import init_integration from tests.common import MockConfigEntry -@pytest.mark.usefixtures("controller") -async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def test_setup_connection_failed( + hass: HomeAssistant, + config_entry: VelbusConfigEntry, + controller: MagicMock, +) -> None: + """Test the setup that fails during velbus connect.""" + controller.return_value.connect.side_effect = VelbusConnectionFailed() + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_start_failed( + hass: HomeAssistant, + config_entry: VelbusConfigEntry, + controller: MagicMock, + entity_registry: er.EntityRegistry, +) -> None: + """Test the setup that fails during velbus start task, should result in no entries.""" + controller.return_value.start.side_effect = ConnectionError() + await init_integration(hass, config_entry) + assert config_entry.state is ConfigEntryState.LOADED + assert ( + er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) == [] + ) + + +async def test_unload_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> None: """Test being able to unload an entry.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -29,9 +64,10 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N assert not hass.data.get(DOMAIN) -@pytest.mark.usefixtures("controller") async def test_device_identifier_migration( - hass: HomeAssistant, config_entry: ConfigEntry, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + config_entry: ConfigEntry, + device_registry: dr.DeviceRegistry, ) -> None: """Test being able to unload an entry.""" original_identifiers = {(DOMAIN, "module_address", "module_serial")} @@ -64,8 +100,10 @@ async def test_device_identifier_migration( assert device_entry.sw_version == "module_sw_version" -@pytest.mark.usefixtures("controller") -async def test_migrate_config_entry(hass: HomeAssistant) -> None: +async def test_migrate_config_entry( + hass: HomeAssistant, + controller: MagicMock, +) -> None: """Test successful migration of entry data.""" legacy_config = {CONF_NAME: "fake_name", CONF_PORT: "1.2.3.4:5678"} entry = MockConfigEntry(domain=DOMAIN, unique_id="my own id", data=legacy_config) @@ -79,3 +117,46 @@ async def test_migrate_config_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) assert dict(entry.data) == legacy_config assert entry.version == 2 + + +async def test_api_call( + hass: HomeAssistant, + mock_relay: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test the api call decorator action.""" + await init_integration(hass, config_entry) + + mock_relay.turn_on.side_effect = OSError() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.living_room_relayname"}, + blocking=True, + ) + + +async def test_device_registry( + hass: HomeAssistant, + config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the velbus device registry.""" + await init_integration(hass, config_entry) + + # Ensure devices are correctly registered + device_entries = dr.async_entries_for_config_entry( + device_registry, config_entry.entry_id + ) + assert device_entries == snapshot + + device_parent = device_registry.async_get_device(identifiers={(DOMAIN, "88")}) + assert device_parent.via_device_id is None + + device = device_registry.async_get_device(identifiers={(DOMAIN, "88-9")}) + assert device.via_device_id == device_parent.id + + device_no_sub = device_registry.async_get_device(identifiers={(DOMAIN, "2")}) + assert device_no_sub.via_device_id is None diff --git a/tests/components/velbus/test_light.py b/tests/components/velbus/test_light.py new file mode 100644 index 00000000000..0ce93d6e6bb --- /dev/null +++ b/tests/components/velbus/test_light.py @@ -0,0 +1,142 @@ +"""Velbus light platform tests.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_FLASH, + ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, + FLASH_LONG, + FLASH_SHORT, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.LIGHT]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +async def test_dimmer_actions( + hass: HomeAssistant, + mock_dimmer: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test every supported dimmer action.""" + await init_integration(hass, config_entry) + # turn off + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.dimmer_full_name_dimmer"}, + blocking=True, + ) + mock_dimmer.set_dimmer_state.assert_called_once_with(0, 0) + # turn on without brightness == restore previous brightness + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer_full_name_dimmer", ATTR_TRANSITION: 1}, + blocking=True, + ) + mock_dimmer.restore_dimmer_state.assert_called_once_with(1) + # turn on with brightness == 0 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.dimmer_full_name_dimmer", + ATTR_BRIGHTNESS: 0, + ATTR_TRANSITION: 1, + }, + blocking=True, + ) + mock_dimmer.set_dimmer_state.assert_called_with(0, 1) + assert mock_dimmer.set_dimmer_state.call_count == 2 + # turn on with brightness == 33 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer_full_name_dimmer", ATTR_BRIGHTNESS: 33}, + blocking=True, + ) + mock_dimmer.set_dimmer_state.assert_called_with(12, 0) + assert mock_dimmer.set_dimmer_state.call_count == 3 + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_led_actions( + hass: HomeAssistant, + mock_button: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test every supported button led action.""" + await init_integration(hass, config_entry) + # turn off + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.bedroom_kid_1_led_buttonon"}, + blocking=True, + ) + mock_button.set_led_state.assert_called_once_with("off") + # turn on + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.bedroom_kid_1_led_buttonon"}, + blocking=True, + ) + mock_button.set_led_state.assert_called_with("on") + assert mock_button.set_led_state.call_count == 2 + # turn on with FLASH_LONG + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.bedroom_kid_1_led_buttonon", ATTR_FLASH: FLASH_LONG}, + blocking=True, + ) + mock_button.set_led_state.assert_called_with("slow") + assert mock_button.set_led_state.call_count == 3 + # turn on with FLASH_SHORT + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.bedroom_kid_1_led_buttonon", ATTR_FLASH: FLASH_SHORT}, + blocking=True, + ) + mock_button.set_led_state.assert_called_with("fast") + assert mock_button.set_led_state.call_count == 4 + # turn on with UNKNOWN flash option + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.bedroom_kid_1_led_buttonon", ATTR_FLASH: FLASH_SHORT}, + blocking=True, + ) + mock_button.set_led_state.assert_called_with("fast") + assert mock_button.set_led_state.call_count == 5 diff --git a/tests/components/velbus/test_select.py b/tests/components/velbus/test_select.py new file mode 100644 index 00000000000..782ae53d440 --- /dev/null +++ b/tests/components/velbus/test_select.py @@ -0,0 +1,52 @@ +"""Velbus select platform tests.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.SELECT]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize( + ("set_program"), [("none"), ("summer"), ("winter"), ("holiday")] +) +async def test_select_program( + hass: HomeAssistant, + mock_select: AsyncMock, + config_entry: MockConfigEntry, + set_program: str, +) -> None: + """Test program selection.""" + await init_integration(hass, config_entry) + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.kitchen_select", ATTR_OPTION: set_program}, + blocking=True, + ) + mock_select.set_selected_program.assert_called_once_with(set_program) diff --git a/tests/components/velbus/test_sensor.py b/tests/components/velbus/test_sensor.py new file mode 100644 index 00000000000..d89d2de59db --- /dev/null +++ b/tests/components/velbus/test_sensor.py @@ -0,0 +1,26 @@ +"""Velbus sensor platform tests.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) diff --git a/tests/components/velbus/test_services.py b/tests/components/velbus/test_services.py new file mode 100644 index 00000000000..94ba91e6dc3 --- /dev/null +++ b/tests/components/velbus/test_services.py @@ -0,0 +1,172 @@ +"""Velbus services tests.""" + +from unittest.mock import AsyncMock + +import pytest +import voluptuous as vol + +from homeassistant.components.velbus.const import ( + CONF_CONFIG_ENTRY, + CONF_INTERFACE, + CONF_MEMO_TEXT, + DOMAIN, + SERVICE_CLEAR_CACHE, + SERVICE_SCAN, + SERVICE_SET_MEMO_TEXT, + SERVICE_SYNC, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import issue_registry as ir + +from . import init_integration + +from tests.common import MockConfigEntry + + +async def test_global_services_with_interface( + hass: HomeAssistant, + config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test services directed at the bus with an interface parameter.""" + await init_integration(hass, config_entry) + + await hass.services.async_call( + DOMAIN, + SERVICE_SCAN, + {CONF_INTERFACE: config_entry.data["port"]}, + blocking=True, + ) + config_entry.runtime_data.controller.scan.assert_called_once_with() + assert issue_registry.async_get_issue(DOMAIN, "deprecated_interface_parameter") + + await hass.services.async_call( + DOMAIN, + SERVICE_SYNC, + {CONF_INTERFACE: config_entry.data["port"]}, + blocking=True, + ) + config_entry.runtime_data.controller.sync_clock.assert_called_once_with() + + # Test invalid interface + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SCAN, + {CONF_INTERFACE: "nonexistent"}, + blocking=True, + ) + + # Test missing interface + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SCAN, + {}, + blocking=True, + ) + + +async def test_global_survices_with_config_entry( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test services directed at the bus with a config_entry.""" + await init_integration(hass, config_entry) + + await hass.services.async_call( + DOMAIN, + SERVICE_SCAN, + {CONF_CONFIG_ENTRY: config_entry.entry_id}, + blocking=True, + ) + config_entry.runtime_data.controller.scan.assert_called_once_with() + + await hass.services.async_call( + DOMAIN, + SERVICE_SYNC, + {CONF_CONFIG_ENTRY: config_entry.entry_id}, + blocking=True, + ) + config_entry.runtime_data.controller.sync_clock.assert_called_once_with() + + # Test invalid interface + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + SERVICE_SCAN, + {CONF_CONFIG_ENTRY: "nonexistent"}, + blocking=True, + ) + + # Test missing interface + with pytest.raises(vol.error.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SCAN, + {}, + blocking=True, + ) + + +async def test_set_memo_text( + hass: HomeAssistant, + config_entry: MockConfigEntry, + controller: AsyncMock, +) -> None: + """Test the set_memo_text service.""" + await init_integration(hass, config_entry) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MEMO_TEXT, + { + CONF_CONFIG_ENTRY: config_entry.entry_id, + CONF_MEMO_TEXT: "Test", + CONF_ADDRESS: 1, + }, + blocking=True, + ) + config_entry.runtime_data.controller.get_module( + 1 + ).set_memo_text.assert_called_once_with("Test") + + # Test with unfound module + controller.return_value.get_module.return_value = None + with pytest.raises(ServiceValidationError, match="Module not found"): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MEMO_TEXT, + { + CONF_CONFIG_ENTRY: config_entry.entry_id, + CONF_MEMO_TEXT: "Test", + CONF_ADDRESS: 2, + }, + blocking=True, + ) + + +async def test_clear_cache( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test the clear_cache service.""" + await init_integration(hass, config_entry) + + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_CACHE, + {CONF_CONFIG_ENTRY: config_entry.entry_id}, + blocking=True, + ) + config_entry.runtime_data.controller.scan.assert_called_once_with() + + await hass.services.async_call( + DOMAIN, + SERVICE_CLEAR_CACHE, + {CONF_CONFIG_ENTRY: config_entry.entry_id, CONF_ADDRESS: 1}, + blocking=True, + ) + assert config_entry.runtime_data.controller.scan.call_count == 2 diff --git a/tests/components/velbus/test_switch.py b/tests/components/velbus/test_switch.py new file mode 100644 index 00000000000..ebb1da084c4 --- /dev/null +++ b/tests/components/velbus/test_switch.py @@ -0,0 +1,57 @@ +"""Velbus switch platform tests.""" + +from unittest.mock import AsyncMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.velbus.PLATFORMS", [Platform.SWITCH]): + await init_integration(hass, config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +async def test_switch_on_off( + hass: HomeAssistant, + mock_relay: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test switching relay on and off press.""" + await init_integration(hass, config_entry) + # turn off + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.living_room_relayname"}, + blocking=True, + ) + mock_relay.turn_off.assert_called_once_with() + # turn on + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.living_room_relayname"}, + blocking=True, + ) + mock_relay.turn_on.assert_called_once_with() diff --git a/tests/components/velux/conftest.py b/tests/components/velux/conftest.py index 512b2a007ed..c88a21d2bba 100644 --- a/tests/components/velux/conftest.py +++ b/tests/components/velux/conftest.py @@ -5,6 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.velux import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD + +from tests.common import MockConfigEntry + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -13,3 +18,44 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.velux.async_setup_entry", return_value=True ) as mock_setup_entry: yield mock_setup_entry + + +@pytest.fixture +def mock_velux_client() -> Generator[AsyncMock]: + """Mock a Velux client.""" + with ( + patch( + "homeassistant.components.velux.config_flow.PyVLX", + autospec=True, + ) as mock_client, + ): + client = mock_client.return_value + yield client + + +@pytest.fixture +def mock_user_config_entry() -> MockConfigEntry: + """Return the user config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="127.0.0.1", + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + }, + ) + + +@pytest.fixture +def mock_discovered_config_entry() -> MockConfigEntry: + """Return the user config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="127.0.0.1", + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + CONF_MAC: "64:61:84:00:ab:cd", + }, + unique_id="VELUX_KLF_ABCD", + ) diff --git a/tests/components/velux/test_config_flow.py b/tests/components/velux/test_config_flow.py index 5f7932d358a..22ad10e1188 100644 --- a/tests/components/velux/test_config_flow.py +++ b/tests/components/velux/test_config_flow.py @@ -2,86 +2,288 @@ from __future__ import annotations -from copy import deepcopy -from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from pyvlx import PyVLXException from homeassistant.components.velux import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry -DUMMY_DATA: dict[str, Any] = { - CONF_HOST: "127.0.0.1", - CONF_PASSWORD: "NotAStrongPassword", -} - -PYVLX_CONFIG_FLOW_CONNECT_FUNCTION_PATH = ( - "homeassistant.components.velux.config_flow.PyVLX.connect" +DHCP_DISCOVERY = DhcpServiceInfo( + ip="127.0.0.1", + hostname="VELUX_KLF_LAN_ABCD", + macaddress="64618400abcd", ) -PYVLX_CONFIG_FLOW_CLASS_PATH = "homeassistant.components.velux.config_flow.PyVLX" - -error_types_to_test: list[tuple[Exception, str]] = [ - (PyVLXException("DUMMY"), "cannot_connect"), - (Exception("DUMMY"), "unknown"), -] - -pytest.mark.usefixtures("mock_setup_entry") -async def test_user_success(hass: HomeAssistant) -> None: +async def test_user_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_velux_client: AsyncMock, +) -> None: """Test starting a flow by user with valid values.""" - with patch(PYVLX_CONFIG_FLOW_CLASS_PATH, autospec=True) as client_mock: - result: dict[str, Any] = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - client_mock.return_value.disconnect.assert_called_once() - client_mock.return_value.connect.assert_called_once() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == DUMMY_DATA[CONF_HOST] - assert result["data"] == DUMMY_DATA + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "127.0.0.1" + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + } + assert not result["result"].unique_id + + mock_velux_client.disconnect.assert_called_once() + mock_velux_client.connect.assert_called_once() -@pytest.mark.parametrize(("error", "error_name"), error_types_to_test) +@pytest.mark.parametrize( + ("exception", "error"), + [ + (PyVLXException("DUMMY"), "cannot_connect"), + (Exception("DUMMY"), "unknown"), + ], +) async def test_user_errors( - hass: HomeAssistant, error: Exception, error_name: str + hass: HomeAssistant, + mock_velux_client: AsyncMock, + exception: Exception, + error: str, + mock_setup_entry: AsyncMock, ) -> None: """Test starting a flow by user but with exceptions.""" - with patch( - PYVLX_CONFIG_FLOW_CONNECT_FUNCTION_PATH, side_effect=error - ) as connect_mock: - result: dict[str, Any] = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) - ) - connect_mock.assert_called_once() + mock_velux_client.connect.side_effect = exception - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": error_name} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": error} + + mock_velux_client.connect.assert_called_once() + + mock_velux_client.connect.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY -async def test_flow_duplicate_entry(hass: HomeAssistant) -> None: +async def test_user_flow_duplicate_entry( + hass: HomeAssistant, + mock_user_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: """Test initialized flow with a duplicate entry.""" - with patch(PYVLX_CONFIG_FLOW_CLASS_PATH, autospec=True): - conf_entry: MockConfigEntry = MockConfigEntry( - domain=DOMAIN, title=DUMMY_DATA[CONF_HOST], data=DUMMY_DATA - ) + mock_user_config_entry.add_to_hass(hass) - conf_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=DUMMY_DATA, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "NotAStrongPassword", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery( + hass: HomeAssistant, + mock_velux_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we can setup from dhcp discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "NotAStrongPassword"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "VELUX_KLF_ABCD" + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_MAC: "64:61:84:00:ab:cd", + CONF_NAME: "VELUX_KLF_ABCD", + CONF_PASSWORD: "NotAStrongPassword", + } + assert result["result"].unique_id == "VELUX_KLF_ABCD" + + mock_velux_client.disconnect.assert_called() + mock_velux_client.connect.assert_called() + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (PyVLXException("DUMMY"), "cannot_connect"), + (Exception("DUMMY"), "unknown"), + ], +) +async def test_dhcp_discovery_errors( + hass: HomeAssistant, + mock_velux_client: AsyncMock, + exception: Exception, + error: str, + mock_setup_entry: AsyncMock, +) -> None: + """Test we can setup from dhcp discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + mock_velux_client.connect.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "NotAStrongPassword"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert result["errors"] == {"base": error} + + mock_velux_client.connect.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "NotAStrongPassword"}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "VELUX_KLF_ABCD" + assert result["data"] == { + CONF_HOST: "127.0.0.1", + CONF_MAC: "64:61:84:00:ab:cd", + CONF_NAME: "VELUX_KLF_ABCD", + CONF_PASSWORD: "NotAStrongPassword", + } + + +async def test_dhcp_discovery_already_configured( + hass: HomeAssistant, + mock_velux_client: AsyncMock, + mock_discovered_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test dhcp discovery when already configured.""" + mock_discovered_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_discover_unique_id( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_velux_client: AsyncMock, + mock_user_config_entry: MockConfigEntry, +) -> None: + """Test dhcp discovery when already configured.""" + mock_user_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_user_config_entry.entry_id) + + assert mock_user_config_entry.state is ConfigEntryState.LOADED + assert mock_user_config_entry.unique_id is None + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_user_config_entry.unique_id == "VELUX_KLF_ABCD" + + +async def test_dhcp_discovery_not_loaded( + hass: HomeAssistant, + mock_velux_client: AsyncMock, + mock_user_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test dhcp discovery when entry with same host not loaded.""" + mock_user_config_entry.add_to_hass(hass) + + assert mock_user_config_entry.state is not ConfigEntryState.LOADED + assert mock_user_config_entry.unique_id is None + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_user_config_entry.unique_id is None diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index e6dd11669d1..eb7e3eb1811 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -8,7 +8,6 @@ import pytest from verisure import Error as VerisureError, LoginError as VerisureLoginError from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.verisure.const import ( CONF_GIID, CONF_LOCK_CODE_DIGITS, @@ -18,6 +17,7 @@ from homeassistant.components.verisure.const import ( from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -333,7 +333,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: """Test that DHCP discovery works.""" result = await hass.config_entries.flow.async_init( DOMAIN, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.4", macaddress="0123456789ab", hostname="mock_hostname" ), context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/vesync/common.py b/tests/components/vesync/common.py index 94e1511ce19..39a92778727 100644 --- a/tests/components/vesync/common.py +++ b/tests/components/vesync/common.py @@ -1,20 +1,27 @@ """Common methods used across tests for VeSync.""" import json +from typing import Any import requests_mock from homeassistant.components.vesync.const import DOMAIN +from homeassistant.util.json import JsonObjectType from tests.common import load_fixture, load_json_object_fixture +ENTITY_HUMIDIFIER = "humidifier.humidifier_200s" +ENTITY_HUMIDIFIER_MIST_LEVEL = "number.humidifier_200s_mist_level" +ENTITY_HUMIDIFIER_HUMIDITY = "sensor.humidifier_200s_humidity" +ENTITY_HUMIDIFIER_300S_NIGHT_LIGHT_SELECT = "select.humidifier_300s_night_light_level" + ALL_DEVICES = load_json_object_fixture("vesync-devices.json", DOMAIN) ALL_DEVICE_NAMES: list[str] = [ dev["deviceName"] for dev in ALL_DEVICES["result"]["list"] ] DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = { "Humidifier 200s": [ - ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") + ("post", "/cloud/v2/deviceManaged/bypassV2", "humidifier-200s.json") ], "Humidifier 600S": [ ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") @@ -26,7 +33,7 @@ DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = { ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") ], "Air Purifier 400s": [ - ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") + ("post", "/cloud/v2/deviceManaged/bypassV2", "air-purifier-400s-detail.json") ], "Air Purifier 600s": [ ("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json") @@ -37,11 +44,17 @@ DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = { "Temperature Light": [ ("post", "/cloud/v1/deviceManaged/bypass", "device-detail.json") ], - "Outlet": [("get", "/v1/device/outlet/detail", "outlet-detail.json")], + "Outlet": [ + ("get", "/v1/device/outlet/detail", "outlet-detail.json"), + ("get", "/v1/device/outlet/energy/week", "outlet-energy-week.json"), + ], "Wall Switch": [ ("post", "/inwallswitch/v1/device/devicedetail", "device-detail.json") ], "Dimmer Switch": [("post", "/dimmer/v1/device/devicedetail", "dimmer-detail.json")], + "SmartTowerFan": [ + ("post", "/cloud/v2/deviceManaged/bypassV2", "SmartTowerFan-detail.json") + ], } @@ -71,6 +84,99 @@ def mock_devices_response( ) +def mock_multiple_device_responses( + requests_mock: requests_mock.Mocker, device_names: list[str] +) -> None: + """Build a response for the Helpers.call_api method for multiple devices.""" + device_list = [ + device + for device in ALL_DEVICES["result"]["list"] + if device["deviceName"] in device_names + ] + + requests_mock.post( + "https://smartapi.vesync.com/cloud/v1/deviceManaged/devices", + json={"code": 0, "result": {"list": device_list}}, + ) + requests_mock.post( + "https://smartapi.vesync.com/cloud/v1/user/login", + json=load_json_object_fixture("vesync-login.json", DOMAIN), + ) + for device_name in device_names: + for fixture in DEVICE_FIXTURES[device_name]: + requests_mock.request( + fixture[0], + f"https://smartapi.vesync.com{fixture[1]}", + json=load_json_object_fixture(fixture[2], DOMAIN), + ) + + +def mock_air_purifier_400s_update_response(requests_mock: requests_mock.Mocker) -> None: + """Build a response for the Helpers.call_api method for air_purifier_400s with updated data.""" + + device_name = "Air Purifier 400s" + for fixture in DEVICE_FIXTURES[device_name]: + requests_mock.request( + fixture[0], + f"https://smartapi.vesync.com{fixture[1]}", + json=load_json_object_fixture( + "air-purifier-400s-detail-updated.json", DOMAIN + ), + ) + + +def mock_device_response( + requests_mock: requests_mock.Mocker, device_name: str, override: Any +) -> None: + """Build a response for the Helpers.call_api method with updated data.""" + + def load_and_merge(source: str) -> JsonObjectType: + json = load_json_object_fixture(source, DOMAIN) + + if override: + json.update(override) + + return json + + fixtures = DEVICE_FIXTURES[device_name] + + # The first item contain basic device details + if len(fixtures) > 0: + item = fixtures[0] + + requests_mock.request( + item[0], + f"https://smartapi.vesync.com{item[1]}", + json=load_and_merge(item[2]), + ) + + +def mock_outlet_energy_response( + requests_mock: requests_mock.Mocker, device_name: str, override: Any +) -> None: + """Build a response for the Helpers.call_api energy request with updated data.""" + + def load_and_merge(source: str) -> JsonObjectType: + json = load_json_object_fixture(source, DOMAIN) + + if override: + json.update(override) + + return json + + fixtures = DEVICE_FIXTURES[device_name] + + # The 2nd item contain energy details + if len(fixtures) > 1: + item = fixtures[1] + + requests_mock.request( + item[0], + f"https://smartapi.vesync.com{item[1]}", + json=load_and_merge(item[2]), + ) + + def call_api_side_effect__no_devices(*args, **kwargs): """Build a side_effects method for the Helpers.call_api method.""" if args[0] == "/cloud/v1/user/login" and args[1] == "post": diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index 5500ef1a55f..df6ebbdf6e7 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -7,9 +7,10 @@ from unittest.mock import Mock, patch import pytest from pyvesync import VeSync from pyvesync.vesyncbulb import VeSyncBulb -from pyvesync.vesyncfan import VeSyncAirBypass +from pyvesync.vesyncfan import VeSyncAirBypass, VeSyncHumid200300S from pyvesync.vesyncoutlet import VeSyncOutlet from pyvesync.vesyncswitch import VeSyncSwitch +import requests_mock from homeassistant.components.vesync import DOMAIN from homeassistant.config_entries import ConfigEntry @@ -17,6 +18,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from .common import mock_multiple_device_responses + from tests.common import MockConfigEntry @@ -100,3 +103,118 @@ def dimmable_switch_fixture(): def outlet_fixture(): """Create a mock VeSync outlet fixture.""" return Mock(VeSyncOutlet) + + +@pytest.fixture(name="humidifier") +def humidifier_fixture(): + """Create a mock VeSync Classic200S humidifier fixture.""" + return Mock( + VeSyncHumid200300S, + cid="200s-humidifier", + config={ + "auto_target_humidity": 40, + "display": "true", + "automatic_stop": "true", + }, + details={ + "humidity": 35, + "mode": "manual", + }, + device_type="Classic200S", + device_name="Humidifier 200s", + device_status="on", + mist_level=6, + mist_modes=["auto", "manual"], + mode=None, + sub_device_no=0, + config_module="configModule", + connection_status="online", + current_firm_version="1.0.0", + water_lacks=False, + water_tank_lifted=False, + ) + + +@pytest.fixture(name="humidifier_300s") +def humidifier_300s_fixture(): + """Create a mock VeSync Classic300S humidifier fixture.""" + return Mock( + VeSyncHumid200300S, + cid="300s-humidifier", + config={ + "auto_target_humidity": 40, + "display": "true", + "automatic_stop": "true", + }, + details={"humidity": 35, "mode": "manual", "night_light_brightness": 50}, + device_type="Classic300S", + device_name="Humidifier 300s", + device_status="on", + mist_level=6, + mist_modes=["auto", "manual"], + mode=None, + night_light=True, + sub_device_no=0, + config_module="configModule", + connection_status="online", + current_firm_version="1.0.0", + water_lacks=False, + water_tank_lifted=False, + ) + + +@pytest.fixture(name="humidifier_config_entry") +async def humidifier_config_entry( + hass: HomeAssistant, requests_mock: requests_mock.Mocker, config +) -> MockConfigEntry: + """Create a mock VeSync config entry for `Humidifier 200s`.""" + entry = MockConfigEntry( + title="VeSync", + domain=DOMAIN, + data=config[DOMAIN], + ) + entry.add_to_hass(hass) + + device_name = "Humidifier 200s" + mock_multiple_device_responses(requests_mock, [device_name]) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +@pytest.fixture +async def install_humidifier_device( + hass: HomeAssistant, + config_entry: ConfigEntry, + manager, + request: pytest.FixtureRequest, +) -> None: + """Create a mock VeSync config entry with the specified humidifier device.""" + + # Install the defined humidifier + manager._dev_list["fans"].append(request.getfixturevalue(request.param)) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +@pytest.fixture(name="switch_old_id_config_entry") +async def switch_old_id_config_entry( + hass: HomeAssistant, requests_mock: requests_mock.Mocker, config +) -> MockConfigEntry: + """Create a mock VeSync config entry for `switch` with the old unique ID approach.""" + entry = MockConfigEntry( + title="VeSync", + domain=DOMAIN, + data=config[DOMAIN], + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + wall_switch = "Wall Switch" + humidifer = "Humidifier 200s" + + mock_multiple_device_responses(requests_mock, [wall_switch, humidifer]) + + return entry diff --git a/tests/components/vesync/fixtures/SmartTowerFan-detail.json b/tests/components/vesync/fixtures/SmartTowerFan-detail.json new file mode 100644 index 00000000000..061dcb5b0d0 --- /dev/null +++ b/tests/components/vesync/fixtures/SmartTowerFan-detail.json @@ -0,0 +1,37 @@ +{ + "traceId": "0000000000", + "code": 0, + "msg": "request success", + "module": null, + "stacktrace": null, + "result": { + "traceId": "0000000000", + "code": 0, + "result": { + "powerSwitch": 0, + "workMode": "normal", + "manualSpeedLevel": 1, + "fanSpeedLevel": 0, + "screenState": 0, + "screenSwitch": 0, + "oscillationSwitch": 1, + "oscillationState": 1, + "muteSwitch": 1, + "muteState": 1, + "timerRemain": 0, + "temperature": 717, + "humidity": 40, + "thermalComfort": 65, + "errorCode": 0, + "sleepPreference": { + "sleepPreferenceType": "default", + "oscillationSwitch": 0, + "initFanSpeedLevel": 0, + "fallAsleepRemain": 0, + "autoChangeFanLevelSwitch": 0 + }, + "scheduleCount": 0, + "displayingType": 0 + } + } +} diff --git a/tests/components/vesync/fixtures/air-purifier-400s-detail-updated.json b/tests/components/vesync/fixtures/air-purifier-400s-detail-updated.json new file mode 100644 index 00000000000..b48eefba4c9 --- /dev/null +++ b/tests/components/vesync/fixtures/air-purifier-400s-detail-updated.json @@ -0,0 +1,39 @@ +{ + "code": 0, + "brightNess": "50", + "result": { + "light": { + "brightness": 50, + "colorTempe": 5400 + }, + "result": { + "brightness": 50, + "red": 178.5, + "green": 255, + "blue": 25.5, + "colorMode": "rgb", + "humidity": 35, + "mist_virtual_level": 6, + "mode": "manual", + "water_lacks": true, + "water_tank_lifted": true, + "automatic_stop_reach_target": true, + "night_light_brightness": 10, + "enabled": true, + "filter_life": 99, + "level": 1, + "display": true, + "display_forever": false, + "child_lock": false, + "night_light": "on", + "air_quality": 15, + "air_quality_value": 1, + "configuration": { + "auto_target_humidity": 40, + "display": true, + "automatic_stop": true + } + }, + "code": 0 + } +} diff --git a/tests/components/vesync/fixtures/air-purifier-400s-detail.json b/tests/components/vesync/fixtures/air-purifier-400s-detail.json new file mode 100644 index 00000000000..a26d9e2a975 --- /dev/null +++ b/tests/components/vesync/fixtures/air-purifier-400s-detail.json @@ -0,0 +1,39 @@ +{ + "code": 0, + "brightNess": "50", + "result": { + "light": { + "brightness": 50, + "colorTempe": 5400 + }, + "result": { + "brightness": 50, + "red": 178.5, + "green": 255, + "blue": 25.5, + "colorMode": "rgb", + "humidity": 35, + "mist_virtual_level": 6, + "mode": "manual", + "water_lacks": true, + "water_tank_lifted": true, + "automatic_stop_reach_target": true, + "night_light_brightness": 10, + "enabled": true, + "filter_life": 99, + "level": 1, + "display": true, + "display_forever": false, + "child_lock": false, + "night_light": "off", + "air_quality": 5, + "air_quality_value": 1, + "configuration": { + "auto_target_humidity": 40, + "display": true, + "automatic_stop": true + } + }, + "code": 0 + } +} diff --git a/tests/components/vesync/fixtures/humidifier-200s.json b/tests/components/vesync/fixtures/humidifier-200s.json new file mode 100644 index 00000000000..a0a98bde110 --- /dev/null +++ b/tests/components/vesync/fixtures/humidifier-200s.json @@ -0,0 +1,27 @@ +{ + "code": 0, + "result": { + "result": { + "humidity": 35, + "mist_level": 6, + "mist_virtual_level": 6, + "mode": "manual", + "water_lacks": true, + "water_tank_lifted": true, + "automatic_stop_reach_target": true, + "night_light_brightness": 10, + "enabled": true, + "level": 1, + "display": true, + "display_forever": false, + "child_lock": false, + "night_light": "off", + "configuration": { + "auto_target_humidity": 40, + "display": true, + "automatic_stop": true + } + }, + "code": 0 + } +} diff --git a/tests/components/vesync/fixtures/outlet-energy-week.json b/tests/components/vesync/fixtures/outlet-energy-week.json new file mode 100644 index 00000000000..6e23be2e197 --- /dev/null +++ b/tests/components/vesync/fixtures/outlet-energy-week.json @@ -0,0 +1,7 @@ +{ + "energyConsumptionOfToday": 1, + "costPerKWH": 0.15, + "maxEnergy": 6, + "totalEnergy": 0, + "currency": "$" +} diff --git a/tests/components/vesync/fixtures/vesync-devices.json b/tests/components/vesync/fixtures/vesync-devices.json index 699084507ea..3109fd3ea40 100644 --- a/tests/components/vesync/fixtures/vesync-devices.json +++ b/tests/components/vesync/fixtures/vesync-devices.json @@ -6,10 +6,11 @@ "cid": "200s-humidifier", "deviceType": "Classic200S", "deviceName": "Humidifier 200s", - "subDeviceNo": null, + "subDeviceNo": 4321, "deviceStatus": "on", "connectionStatus": "online", - "uuid": "00000000-1111-2222-3333-444444444444" + "uuid": "00000000-1111-2222-3333-444444444444", + "configModule": "configModule" }, { "cid": "600s-humidifier", @@ -30,7 +31,8 @@ "deviceName": "Air Purifier 131s", "subDeviceNo": null, "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55", @@ -39,7 +41,8 @@ "subDeviceNo": null, "deviceStatus": "on", "type": "wifi-air", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "400s-purifier", @@ -48,7 +51,8 @@ "subDeviceNo": null, "deviceStatus": "on", "type": "wifi-air", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "600s-purifier", @@ -57,7 +61,8 @@ "subDeviceNo": null, "type": "wifi-air", "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "dimmable-bulb", @@ -65,7 +70,8 @@ "deviceName": "Dimmable Light", "subDeviceNo": null, "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "tunable-bulb", @@ -73,7 +79,8 @@ "deviceName": "Temperature Light", "subDeviceNo": null, "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "outlet", @@ -81,7 +88,8 @@ "deviceName": "Outlet", "subDeviceNo": null, "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "switch", @@ -89,7 +97,8 @@ "deviceName": "Wall Switch", "subDeviceNo": null, "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" }, { "cid": "dimmable-switch", @@ -97,7 +106,17 @@ "deviceName": "Dimmer Switch", "subDeviceNo": null, "deviceStatus": "on", - "connectionStatus": "online" + "connectionStatus": "online", + "configModule": "configModule" + }, + { + "cid": "smarttowerfan", + "deviceType": "LTF-F422S-KEU", + "deviceName": "SmartTowerFan", + "subDeviceNo": null, + "deviceStatus": "on", + "connectionStatus": "online", + "configModule": "configModule" } ] } diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index 54ed8acf2d7..1c409dbab00 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -128,7 +128,7 @@ 'sleep', 'manual', ]), - 'mode': None, + 'mode': 'humidity', 'night_light': True, 'pid': None, 'speed': None, @@ -160,6 +160,30 @@ # --- # name: test_async_get_device_diagnostics__single_fan dict({ + '_config_dict': dict({ + 'features': list([ + 'air_quality', + ]), + 'levels': list([ + 1, + 2, + ]), + 'models': list([ + 'LV-PUR131S', + 'LV-RH131S', + ]), + 'modes': list([ + 'manual', + 'auto', + 'sleep', + 'off', + ]), + 'module': 'VeSyncAir131', + }), + '_features': list([ + 'air_quality', + ]), + 'air_quality_feature': True, 'cid': 'abcdefghabcdefghabcdefghabcdefgh', 'config': dict({ }), @@ -180,6 +204,7 @@ 'device_region': 'US', 'device_status': 'unknown', 'device_type': 'LV-PUR131S', + 'enabled': True, 'extension': None, 'home_assistant': dict({ 'disabled': False, @@ -271,6 +296,12 @@ 'mac_id': '**REDACTED**', 'manager': '**REDACTED**', 'mode': None, + 'modes': list([ + 'manual', + 'auto', + 'sleep', + 'off', + ]), 'pid': None, 'speed': None, 'sub_device_no': None, diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 60af4ae3d5b..0b56a08eeff 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -46,6 +47,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -96,6 +98,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -137,6 +140,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -193,6 +197,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -235,6 +240,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -292,6 +298,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -334,6 +341,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -391,6 +399,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -429,6 +438,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -464,6 +474,37 @@ # --- # name: test_fan_state[Humidifier 200s][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '200s-humidifier4321', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'Classic200S', + 'model_id': None, + 'name': 'Humidifier 200s', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_fan_state[Humidifier 200s][entities] @@ -472,6 +513,37 @@ # --- # name: test_fan_state[Humidifier 600S][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '600s-humidifier', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LUH-A602S-WUS', + 'model_id': None, + 'name': 'Humidifier 600S', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_fan_state[Humidifier 600S][entities] @@ -483,6 +555,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -516,11 +589,117 @@ list([ ]) # --- +# name: test_fan_state[SmartTowerFan][devices] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + 'smarttowerfan', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LTF-F422S-KEU', + 'model_id': None, + 'name': 'SmartTowerFan', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_fan_state[SmartTowerFan][entities] + list([ + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'advancedSleep', + 'auto', + 'turbo', + 'normal', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.smarttowerfan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'vesync', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'vesync', + 'unique_id': 'smarttowerfan', + 'unit_of_measurement': None, + }), + ]) +# --- +# name: test_fan_state[SmartTowerFan][fan.smarttowerfan] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'child_lock': False, + 'friendly_name': 'SmartTowerFan', + 'mode': 'normal', + 'night_light': 'off', + 'percentage': None, + 'percentage_step': 7.6923076923076925, + 'preset_mode': None, + 'preset_modes': list([ + 'advancedSleep', + 'auto', + 'turbo', + 'normal', + ]), + 'screen_status': False, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.smarttowerfan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_fan_state[Temperature Light][devices] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -559,6 +738,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index 2e7fe9ac1bb..bed711b1040 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -42,6 +43,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -80,6 +82,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -118,6 +121,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -156,6 +160,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -197,6 +202,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -246,6 +252,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -287,6 +294,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +343,37 @@ # --- # name: test_light_state[Humidifier 200s][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '200s-humidifier4321', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'Classic200S', + 'model_id': None, + 'name': 'Humidifier 200s', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_light_state[Humidifier 200s][entities] @@ -343,6 +382,37 @@ # --- # name: test_light_state[Humidifier 600S][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '600s-humidifier', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LUH-A602S-WUS', + 'model_id': None, + 'name': 'Humidifier 600S', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_light_state[Humidifier 600S][entities] @@ -354,6 +424,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -387,11 +458,51 @@ list([ ]) # --- +# name: test_light_state[SmartTowerFan][devices] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + 'smarttowerfan', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LTF-F422S-KEU', + 'model_id': None, + 'name': 'SmartTowerFan', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_light_state[SmartTowerFan][entities] + list([ + ]) +# --- # name: test_light_state[Temperature Light][devices] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -437,6 +548,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -497,6 +609,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 11d931e023a..c701fa8a324 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -43,6 +44,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -74,6 +76,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -134,6 +137,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -173,6 +177,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -220,6 +225,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -259,6 +265,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -290,6 +297,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -323,6 +331,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -399,6 +408,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -438,6 +448,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -469,6 +480,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -502,6 +514,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -578,6 +591,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -616,6 +630,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -651,25 +666,188 @@ # --- # name: test_sensor_state[Humidifier 200s][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '200s-humidifier4321', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'Classic200S', + 'model_id': None, + 'name': 'Humidifier 200s', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_sensor_state[Humidifier 200s][entities] list([ + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.humidifier_200s_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'vesync', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '200s-humidifier4321-humidity', + 'unit_of_measurement': '%', + }), ]) # --- +# name: test_sensor_state[Humidifier 200s][sensor.humidifier_200s_humidity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Humidifier 200s Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.humidifier_200s_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35', + }) +# --- # name: test_sensor_state[Humidifier 600S][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '600s-humidifier', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LUH-A602S-WUS', + 'model_id': None, + 'name': 'Humidifier 600S', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_sensor_state[Humidifier 600S][entities] list([ + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.humidifier_600s_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'vesync', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '600s-humidifier-humidity', + 'unit_of_measurement': '%', + }), ]) # --- +# name: test_sensor_state[Humidifier 600S][sensor.humidifier_600s_humidity] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Humidifier 600S Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.humidifier_600s_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35', + }) +# --- # name: test_sensor_state[Outlet][devices] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -709,6 +887,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -742,6 +921,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -775,6 +955,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -808,6 +989,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -841,6 +1023,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -874,6 +1057,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -997,11 +1181,51 @@ 'state': '0', }) # --- +# name: test_sensor_state[SmartTowerFan][devices] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + 'smarttowerfan', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LTF-F422S-KEU', + 'model_id': None, + 'name': 'SmartTowerFan', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_sensor_state[SmartTowerFan][entities] + list([ + ]) +# --- # name: test_sensor_state[Temperature Light][devices] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -1040,6 +1264,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index 4b271ee55d9..1faed941338 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -4,6 +4,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -42,6 +43,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -80,6 +82,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -118,6 +121,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -156,6 +160,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -194,6 +199,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -229,6 +235,37 @@ # --- # name: test_switch_state[Humidifier 200s][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '200s-humidifier4321', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'Classic200S', + 'model_id': None, + 'name': 'Humidifier 200s', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_switch_state[Humidifier 200s][entities] @@ -237,6 +274,37 @@ # --- # name: test_switch_state[Humidifier 600S][devices] list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + '600s-humidifier', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LUH-A602S-WUS', + 'model_id': None, + 'name': 'Humidifier 600S', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), ]) # --- # name: test_switch_state[Humidifier 600S][entities] @@ -248,6 +316,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -285,6 +354,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -300,14 +370,14 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'outlet', + 'unique_id': 'outlet-device_status', 'unit_of_measurement': None, }), ]) @@ -315,6 +385,7 @@ # name: test_switch_state[Outlet][switch.outlet] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', 'friendly_name': 'Outlet', }), 'context': , @@ -325,11 +396,51 @@ 'state': 'on', }) # --- +# name: test_switch_state[SmartTowerFan][devices] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'vesync', + 'smarttowerfan', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'VeSync', + 'model': 'LTF-F422S-KEU', + 'model_id': None, + 'name': 'SmartTowerFan', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_switch_state[SmartTowerFan][entities] + list([ + ]) +# --- # name: test_switch_state[Temperature Light][devices] list([ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -368,6 +479,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -405,6 +517,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -420,14 +533,14 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': None, - 'unique_id': 'switch', + 'unique_id': 'switch-device_status', 'unit_of_measurement': None, }), ]) @@ -435,6 +548,7 @@ # name: test_switch_state[Wall Switch][switch.wall_switch] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'switch', 'friendly_name': 'Wall Switch', }), 'context': , diff --git a/tests/components/vesync/test_diagnostics.py b/tests/components/vesync/test_diagnostics.py index b948053c3a0..25aa5337281 100644 --- a/tests/components/vesync/test_diagnostics.py +++ b/tests/components/vesync/test_diagnostics.py @@ -101,6 +101,9 @@ async def test_async_get_device_diagnostics__single_fan( "home_assistant.entities.2.state.last_changed": (str,), "home_assistant.entities.2.state.last_reported": (str,), "home_assistant.entities.2.state.last_updated": (str,), + "home_assistant.entities.3.state.last_changed": (str,), + "home_assistant.entities.3.state.last_reported": (str,), + "home_assistant.entities.3.state.last_updated": (str,), } ) ) diff --git a/tests/components/vesync/test_humidifier.py b/tests/components/vesync/test_humidifier.py new file mode 100644 index 00000000000..d5057c44951 --- /dev/null +++ b/tests/components/vesync/test_humidifier.py @@ -0,0 +1,329 @@ +"""Tests for the humidifier platform.""" + +from contextlib import nullcontext +import logging +from unittest.mock import patch + +import pytest + +from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + ATTR_MODE, + DOMAIN as HUMIDIFIER_DOMAIN, + MODE_AUTO, + MODE_SLEEP, + SERVICE_SET_HUMIDITY, + SERVICE_SET_MODE, +) +from homeassistant.components.vesync.const import ( + VS_HUMIDIFIER_MODE_AUTO, + VS_HUMIDIFIER_MODE_MANUAL, + VS_HUMIDIFIER_MODE_SLEEP, +) +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from .common import ( + ENTITY_HUMIDIFIER, + ENTITY_HUMIDIFIER_HUMIDITY, + ENTITY_HUMIDIFIER_MIST_LEVEL, +) + +from tests.common import MockConfigEntry + +NoException = nullcontext() + + +async def test_humidifier_state( + hass: HomeAssistant, humidifier_config_entry: MockConfigEntry +) -> None: + """Test the resulting setup state is as expected for the platform.""" + + expected_entities = [ + ENTITY_HUMIDIFIER, + ENTITY_HUMIDIFIER_HUMIDITY, + ENTITY_HUMIDIFIER_MIST_LEVEL, + ] + + assert humidifier_config_entry.state is ConfigEntryState.LOADED + + for entity_id in expected_entities: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + state = hass.states.get(ENTITY_HUMIDIFIER) + + # ATTR_HUMIDITY represents the target_humidity which comes from configuration.auto_target_humidity node + assert state.attributes.get(ATTR_HUMIDITY) == 40 + + +async def test_set_target_humidity_invalid( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, +) -> None: + """Test handling of invalid value in set_humidify method.""" + + # Setting value out of range results in ServiceValidationError and + # VeSyncHumid200300S.set_humidity does not get called. + with ( + patch("pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity") as method_mock, + pytest.raises(ServiceValidationError), + ): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER, ATTR_HUMIDITY: 20}, + blocking=True, + ) + await hass.async_block_till_done() + method_mock.assert_not_called() + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(True, NoException), (False, pytest.raises(HomeAssistantError))], +) +async def test_set_target_humidity( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test handling of return value from VeSyncHumid200300S.set_humidity.""" + + # If VeSyncHumid200300S.set_humidity fails (returns False), then HomeAssistantError is raised + with ( + expectation, + patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity", + return_value=api_response, + ) as method_mock, + ): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER, ATTR_HUMIDITY: 54}, + blocking=True, + ) + await hass.async_block_till_done() + method_mock.assert_called_once() + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(False, pytest.raises(HomeAssistantError)), (True, NoException)], +) +async def test_turn_on( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test turn_on method.""" + + # turn_on returns False indicating failure in which case humidifier.turn_on + # raises HomeAssistantError. + with ( + expectation, + patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.turn_on", return_value=api_response + ) as method_mock, + ): + with patch( + "homeassistant.components.vesync.humidifier.VeSyncHumidifierHA.schedule_update_ha_state" + ) as update_mock: + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + update_mock.assert_called_once() + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(False, pytest.raises(HomeAssistantError)), (True, NoException)], +) +async def test_turn_off( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test turn_off method.""" + + # turn_off returns False indicating failure in which case humidifier.turn_off + # raises HomeAssistantError. + with ( + expectation, + patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.turn_off", return_value=api_response + ) as method_mock, + ): + with patch( + "homeassistant.components.vesync.humidifier.VeSyncHumidifierHA.schedule_update_ha_state" + ) as update_mock: + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + update_mock.assert_called_once() + + +async def test_set_mode_invalid( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, +) -> None: + """Test handling of invalid value in set_mode method.""" + + with patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity_mode" + ) as method_mock: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER, ATTR_MODE: "something_invalid"}, + blocking=True, + ) + await hass.async_block_till_done() + method_mock.assert_not_called() + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(True, NoException), (False, pytest.raises(HomeAssistantError))], +) +async def test_set_mode( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test handling of value in set_mode method.""" + + # If VeSyncHumid200300S.set_humidity_mode fails (returns False), then HomeAssistantError is raised + with ( + expectation, + patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity_mode", + return_value=api_response, + ) as method_mock, + ): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER, ATTR_MODE: MODE_AUTO}, + blocking=True, + ) + await hass.async_block_till_done() + method_mock.assert_called_once() + + +async def test_base_unique_id( + hass: HomeAssistant, + humidifier_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that unique_id is based on subDeviceNo.""" + # vesync-device.json defines subDeviceNo for 200s-humidifier as 4321. + entity = entity_registry.async_get(ENTITY_HUMIDIFIER) + assert entity.unique_id.endswith("4321") + + +async def test_invalid_mist_modes( + hass: HomeAssistant, + config_entry: ConfigEntry, + humidifier, + manager, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test unsupported mist mode.""" + + humidifier.mist_modes = ["invalid_mode"] + + with patch( + "homeassistant.components.vesync.async_generate_device_list", + return_value=[humidifier], + ): + caplog.clear() + caplog.set_level(logging.WARNING) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert "Unknown mode 'invalid_mode'" in caplog.text + + +async def test_valid_mist_modes( + hass: HomeAssistant, + config_entry: ConfigEntry, + humidifier, + manager, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test supported mist mode.""" + + humidifier.mist_modes = ["auto", "manual"] + + with patch( + "homeassistant.components.vesync.async_generate_device_list", + return_value=[humidifier], + ): + caplog.clear() + caplog.set_level(logging.WARNING) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert "Unknown mode 'auto'" not in caplog.text + assert "Unknown mode 'manual'" not in caplog.text + + +async def test_set_mode_sleep_turns_display_off( + hass: HomeAssistant, + config_entry: ConfigEntry, + humidifier, + manager, +) -> None: + """Test update of display for sleep mode.""" + + # First define valid mist modes + humidifier.mist_modes = [ + VS_HUMIDIFIER_MODE_AUTO, + VS_HUMIDIFIER_MODE_MANUAL, + VS_HUMIDIFIER_MODE_SLEEP, + ] + + with patch( + "homeassistant.components.vesync.async_generate_device_list", + return_value=[humidifier], + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with ( + patch.object(humidifier, "set_humidity_mode", return_value=True), + patch.object(humidifier, "set_display") as display_mock, + ): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + SERVICE_SET_MODE, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER, ATTR_MODE: MODE_SLEEP}, + blocking=True, + ) + display_mock.assert_called_once_with(False) diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index a089a270c94..011545af2ae 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -5,18 +5,14 @@ from unittest.mock import Mock, patch import pytest from pyvesync import VeSync -from homeassistant.components.vesync import async_setup_entry -from homeassistant.components.vesync.const import ( - DOMAIN, - VS_FANS, - VS_LIGHTS, - VS_MANAGER, - VS_SENSORS, - VS_SWITCHES, -) -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.vesync import SERVICE_UPDATE_DEVS, async_setup_entry +from homeassistant.components.vesync.const import DOMAIN, VS_DEVICES, VS_MANAGER +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry async def test_async_setup_entry__not_login( @@ -30,7 +26,9 @@ async def test_async_setup_entry__not_login( with ( patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock, - patch("homeassistant.components.vesync.async_process_devices") as process_mock, + patch( + "homeassistant.components.vesync.async_generate_device_list" + ) as process_mock, ): assert not await async_setup_entry(hass, config_entry) await hass.async_block_till_done() @@ -52,20 +50,26 @@ async def test_async_setup_entry__no_devices( await hass.async_block_till_done() assert setups_mock.call_count == 1 assert setups_mock.call_args.args[0] == config_entry - assert setups_mock.call_args.args[1] == [] + assert setups_mock.call_args.args[1] == [ + Platform.BINARY_SENSOR, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ] assert manager.login.call_count == 1 assert hass.data[DOMAIN][VS_MANAGER] == manager - assert not hass.data[DOMAIN][VS_SWITCHES] - assert not hass.data[DOMAIN][VS_FANS] - assert not hass.data[DOMAIN][VS_LIGHTS] - assert not hass.data[DOMAIN][VS_SENSORS] + assert not hass.data[DOMAIN][VS_DEVICES] async def test_async_setup_entry__loads_fans( hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync, fan ) -> None: - """Test setup connects to vesync and loads fan platform.""" + """Test setup connects to vesync and loads fan.""" fans = [fan] manager.fans = fans manager._dev_list = { @@ -78,10 +82,103 @@ async def test_async_setup_entry__loads_fans( await hass.async_block_till_done() assert setups_mock.call_count == 1 assert setups_mock.call_args.args[0] == config_entry - assert setups_mock.call_args.args[1] == [Platform.FAN, Platform.SENSOR] + assert setups_mock.call_args.args[1] == [ + Platform.BINARY_SENSOR, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ] assert manager.login.call_count == 1 assert hass.data[DOMAIN][VS_MANAGER] == manager - assert not hass.data[DOMAIN][VS_SWITCHES] - assert hass.data[DOMAIN][VS_FANS] == [fan] - assert not hass.data[DOMAIN][VS_LIGHTS] - assert hass.data[DOMAIN][VS_SENSORS] == [fan] + assert hass.data[DOMAIN][VS_DEVICES] == [fan] + + +async def test_async_new_device_discovery( + hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync, fan, humidifier +) -> None: + """Test new device discovery.""" + + assert await hass.config_entries.async_setup(config_entry.entry_id) + # Assert platforms loaded + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + assert not hass.data[DOMAIN][VS_DEVICES] + + # Mock discovery of new fan which would get added to VS_DEVICES. + with patch( + "homeassistant.components.vesync.async_generate_device_list", + return_value=[fan], + ): + await hass.services.async_call(DOMAIN, SERVICE_UPDATE_DEVS, {}, blocking=True) + + assert manager.login.call_count == 1 + assert hass.data[DOMAIN][VS_MANAGER] == manager + assert hass.data[DOMAIN][VS_DEVICES] == [fan] + + # Mock discovery of new humidifier which would invoke discovery in all platforms. + # The mocked humidifier needs to have all properties populated for correct processing. + with patch( + "homeassistant.components.vesync.async_generate_device_list", + return_value=[humidifier], + ): + await hass.services.async_call(DOMAIN, SERVICE_UPDATE_DEVS, {}, blocking=True) + + assert manager.login.call_count == 1 + assert hass.data[DOMAIN][VS_MANAGER] == manager + assert hass.data[DOMAIN][VS_DEVICES] == [fan, humidifier] + + +async def test_migrate_config_entry( + hass: HomeAssistant, + switch_old_id_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test migration of config entry. Only migrates switches to a new unique_id.""" + switch: er.RegistryEntry = entity_registry.async_get_or_create( + domain="switch", + platform="vesync", + unique_id="switch", + config_entry=switch_old_id_config_entry, + suggested_object_id="switch", + ) + + humidifer: er.RegistryEntry = entity_registry.async_get_or_create( + domain="humidifer", + platform="vesync", + unique_id="humidifer", + config_entry=switch_old_id_config_entry, + suggested_object_id="humidifer", + ) + + assert switch.unique_id == "switch" + assert switch_old_id_config_entry.minor_version == 1 + assert humidifer.unique_id == "humidifer" + + await hass.config_entries.async_setup(switch_old_id_config_entry.entry_id) + await hass.async_block_till_done() + + assert switch_old_id_config_entry.minor_version == 2 + + migrated_switch = entity_registry.async_get(switch.entity_id) + assert migrated_switch is not None + assert migrated_switch.entity_id.startswith("switch") + assert migrated_switch.unique_id == "switch-device_status" + # Confirm humidifer was not impacted + migrated_humidifer = entity_registry.async_get(humidifer.entity_id) + assert migrated_humidifer is not None + assert migrated_humidifer.unique_id == "humidifer" + + # Assert that only one entity exists in the switch domain + switch_entities = [ + e for e in entity_registry.entities.values() if e.domain == "switch" + ] + assert len(switch_entities) == 1 + + humidifer_entities = [ + e for e in entity_registry.entities.values() if e.domain == "humidifer" + ] + assert len(humidifer_entities) == 1 diff --git a/tests/components/vesync/test_number.py b/tests/components/vesync/test_number.py new file mode 100644 index 00000000000..a9230b76db0 --- /dev/null +++ b/tests/components/vesync/test_number.py @@ -0,0 +1,66 @@ +"""Tests for the number platform.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from .common import ENTITY_HUMIDIFIER_MIST_LEVEL + +from tests.common import MockConfigEntry + + +async def test_set_mist_level_bad_range( + hass: HomeAssistant, humidifier_config_entry: MockConfigEntry +) -> None: + """Test set_mist_level invalid value.""" + with ( + pytest.raises(ServiceValidationError), + patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.set_mist_level", + return_value=True, + ) as method_mock, + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER_MIST_LEVEL, ATTR_VALUE: "10"}, + blocking=True, + ) + await hass.async_block_till_done() + method_mock.assert_not_called() + + +async def test_set_mist_level( + hass: HomeAssistant, humidifier_config_entry: MockConfigEntry +) -> None: + """Test set_mist_level usage.""" + + with patch( + "pyvesync.vesyncfan.VeSyncHumid200300S.set_mist_level", + return_value=True, + ) as method_mock: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: ENTITY_HUMIDIFIER_MIST_LEVEL, ATTR_VALUE: "3"}, + blocking=True, + ) + await hass.async_block_till_done() + method_mock.assert_called_once() + + +async def test_mist_level( + hass: HomeAssistant, humidifier_config_entry: MockConfigEntry +) -> None: + """Test the state of mist_level number entity.""" + + assert hass.states.get(ENTITY_HUMIDIFIER_MIST_LEVEL).state == "6" diff --git a/tests/components/vesync/test_platform.py b/tests/components/vesync/test_platform.py new file mode 100644 index 00000000000..fa1e24f4628 --- /dev/null +++ b/tests/components/vesync/test_platform.py @@ -0,0 +1,92 @@ +"""Tests for the coordinator.""" + +from datetime import timedelta + +from freezegun.api import FrozenDateTimeFactory +import requests_mock + +from homeassistant.components.vesync.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from .common import ( + mock_air_purifier_400s_update_response, + mock_device_response, + mock_multiple_device_responses, + mock_outlet_energy_response, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_entity_update( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + requests_mock: requests_mock.Mocker, +) -> None: + """Test Vesync coordinator data update. + + This test sets up a single device `Air Purifier 400s` and then updates it via the coordinator. + """ + + config_data = {CONF_PASSWORD: "username", CONF_USERNAME: "password"} + config_entry = MockConfigEntry( + data=config_data, + domain=DOMAIN, + unique_id="vesync_unique_id_1", + entry_id="1", + ) + + mock_multiple_device_responses(requests_mock, ["Air Purifier 400s", "Outlet"]) + + expected_entities = [ + # From "Air Purifier 400s" + "fan.air_purifier_400s", + "sensor.air_purifier_400s_filter_lifetime", + "sensor.air_purifier_400s_air_quality", + "sensor.air_purifier_400s_pm2_5", + # From Outlet + "switch.outlet", + "sensor.outlet_current_power", + "sensor.outlet_energy_use_today", + "sensor.outlet_energy_use_weekly", + "sensor.outlet_energy_use_monthly", + "sensor.outlet_energy_use_yearly", + "sensor.outlet_current_voltage", + ] + + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + for entity_id in expected_entities: + assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + assert hass.states.get("sensor.air_purifier_400s_air_quality").state == "5" + assert hass.states.get("sensor.outlet_current_voltage").state == "120.0" + assert hass.states.get("sensor.outlet_energy_use_weekly").state == "0" + + # Update the mock responses + mock_air_purifier_400s_update_response(requests_mock) + mock_outlet_energy_response(requests_mock, "Outlet", {"totalEnergy": 2.2}) + mock_device_response(requests_mock, "Outlet", {"voltage": 129}) + + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done(True) + + assert hass.states.get("sensor.air_purifier_400s_air_quality").state == "15" + assert hass.states.get("sensor.outlet_current_voltage").state == "129.0" + + # Test energy update + # pyvesync only updates energy parameters once every 6 hours. + freezer.tick(timedelta(hours=6)) + async_fire_time_changed(hass) + await hass.async_block_till_done(True) + + assert hass.states.get("sensor.air_purifier_400s_air_quality").state == "15" + assert hass.states.get("sensor.outlet_current_voltage").state == "129.0" + assert hass.states.get("sensor.outlet_energy_use_weekly").state == "2.2" diff --git a/tests/components/vesync/test_select.py b/tests/components/vesync/test_select.py new file mode 100644 index 00000000000..30c83c89e0e --- /dev/null +++ b/tests/components/vesync/test_select.py @@ -0,0 +1,54 @@ +"""Tests for the select platform.""" + +import pytest + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.vesync.const import NIGHT_LIGHT_LEVEL_DIM +from homeassistant.components.vesync.select import HA_TO_VS_NIGHT_LIGHT_LEVEL_MAP +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .common import ENTITY_HUMIDIFIER_300S_NIGHT_LIGHT_SELECT + + +@pytest.mark.parametrize( + "install_humidifier_device", ["humidifier_300s"], indirect=True +) +async def test_set_nightlight_level( + hass: HomeAssistant, manager, humidifier_300s, install_humidifier_device +) -> None: + """Test set of night light level.""" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: ENTITY_HUMIDIFIER_300S_NIGHT_LIGHT_SELECT, + ATTR_OPTION: NIGHT_LIGHT_LEVEL_DIM, + }, + blocking=True, + ) + + # Assert that setter API was invoked with the expected translated value + humidifier_300s.set_night_light_brightness.assert_called_once_with( + HA_TO_VS_NIGHT_LIGHT_LEVEL_MAP[NIGHT_LIGHT_LEVEL_DIM] + ) + # Assert that devices were refreshed + manager.update_all_devices.assert_called_once() + + +@pytest.mark.parametrize( + "install_humidifier_device", ["humidifier_300s"], indirect=True +) +async def test_nightlight_level(hass: HomeAssistant, install_humidifier_device) -> None: + """Test the state of night light level select entity.""" + + # The mocked device has night_light_brightness=50 which is "dim" + assert ( + hass.states.get(ENTITY_HUMIDIFIER_300S_NIGHT_LIGHT_SELECT).state + == NIGHT_LIGHT_LEVEL_DIM + ) diff --git a/tests/components/vesync/test_sensor.py b/tests/components/vesync/test_sensor.py index bd3a8eb8591..04d759de584 100644 --- a/tests/components/vesync/test_sensor.py +++ b/tests/components/vesync/test_sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .common import ALL_DEVICE_NAMES, mock_devices_response +from .common import ALL_DEVICE_NAMES, ENTITY_HUMIDIFIER_HUMIDITY, mock_devices_response from tests.common import MockConfigEntry @@ -49,3 +49,11 @@ async def test_sensor_state( # Check states for entity in entities: assert hass.states.get(entity.entity_id) == snapshot(name=entity.entity_id) + + +async def test_humidity( + hass: HomeAssistant, humidifier_config_entry: MockConfigEntry +) -> None: + """Test the state of humidity sensor entity.""" + + assert hass.states.get(ENTITY_HUMIDIFIER_HUMIDITY).state == "35" diff --git a/tests/components/vicare/conftest.py b/tests/components/vicare/conftest.py index aadf85e7081..8e10d2f1a25 100644 --- a/tests/components/vicare/conftest.py +++ b/tests/components/vicare/conftest.py @@ -84,7 +84,7 @@ async def mock_vicare_gas_boiler( """Return a mocked ViCare API representing a single gas boiler device.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with patch( - f"{MODULE}.vicare_login", + f"{MODULE}.login", return_value=MockPyViCare(fixtures), ): await setup_integration(hass, mock_config_entry) @@ -102,7 +102,7 @@ async def mock_vicare_room_sensors( Fixture({"type:climateSensor"}, "vicare/RoomSensor2.json"), ] with patch( - f"{MODULE}.vicare_login", + f"{MODULE}.login", return_value=MockPyViCare(fixtures), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/fixtures/RoomSensor1.json b/tests/components/vicare/fixtures/RoomSensor1.json index b970e54a48c..6c2f38db8d1 100644 --- a/tests/components/vicare/fixtures/RoomSensor1.json +++ b/tests/components/vicare/fixtures/RoomSensor1.json @@ -51,6 +51,24 @@ "timestamp": "2024-03-01T04:40:59.911Z", "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-d87a3bfffe5d844a/features/device.name" }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "zigbee-d87a3bfffe5d844a", + "feature": "device.power.battery", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "level": { + "type": "number", + "unit": "percent", + "value": 89 + } + }, + "timestamp": "2025-02-03T02:30:52.279Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-d87a3bfffe5d844a/features/device.power.battery" + }, { "apiVersion": 1, "commands": {}, diff --git a/tests/components/vicare/fixtures/VitoPure.json b/tests/components/vicare/fixtures/VitoPure.json new file mode 100644 index 00000000000..1e1cdef97ec --- /dev/null +++ b/tests/components/vicare/fixtures/VitoPure.json @@ -0,0 +1,645 @@ +{ + "data": [ + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.levels.levelFour", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-12-17T08:16:15.525Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.levels.levelFour" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.levels.levelOne", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-12-17T08:16:15.525Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.levels.levelOne" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.levels.levelThree", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-12-17T08:16:15.525Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.levels.levelThree" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.levels.levelTwo", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-12-17T08:16:15.525Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.levels.levelTwo" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.operating.modes.filterChange", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.filterChange" + }, + { + "apiVersion": 1, + "commands": { + "setLevel": { + "isExecutable": true, + "name": "setLevel", + "params": { + "level": { + "constraints": { + "enum": ["levelOne", "levelTwo", "levelThree", "levelFour"] + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.permanent/commands/setLevel" + } + }, + "deviceId": "0", + "feature": "ventilation.operating.modes.permanent", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-12-17T13:24:03.411Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.permanent" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.operating.modes.sensorDriven", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-12-17T08:16:15.525Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.sensorDriven" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.operating.modes.ventilation", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.ventilation" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.operating.programs.active", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "levelTwo" + } + }, + "timestamp": "2024-12-17T13:24:03.411Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.programs.active" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": true, + "name": "activate", + "params": { + "timeout": { + "constraints": { + "max": 1440, + "min": 1, + "stepping": 1 + }, + "required": false, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.forcedLevelFour/commands/activate" + }, + "deactivate": { + "isExecutable": true, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.forcedLevelFour/commands/deactivate" + }, + "setDefaultRuntime": { + "isExecutable": true, + "name": "setDefaultRuntime", + "params": { + "defaultRuntime": { + "constraints": { + "max": 1440, + "min": 1, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.forcedLevelFour/commands/setDefaultRuntime" + }, + "setTimeout": { + "isExecutable": true, + "name": "setTimeout", + "params": { + "timeout": { + "constraints": { + "max": 1440, + "min": 1, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.forcedLevelFour/commands/setTimeout" + } + }, + "deviceId": "0", + "feature": "ventilation.quickmodes.forcedLevelFour", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "defaultRuntime": { + "type": "number", + "unit": "minutes", + "value": 30 + }, + "isActiveWritable": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-12-17T13:24:04.515Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.forcedLevelFour" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": true, + "name": "activate", + "params": { + "timeout": { + "constraints": { + "max": 1440, + "min": 1, + "stepping": 1 + }, + "required": false, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.silent/commands/activate" + }, + "deactivate": { + "isExecutable": true, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.silent/commands/deactivate" + }, + "setDefaultRuntime": { + "isExecutable": true, + "name": "setDefaultRuntime", + "params": { + "defaultRuntime": { + "constraints": { + "max": 1440, + "min": 1, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.silent/commands/setDefaultRuntime" + }, + "setTimeout": { + "isExecutable": true, + "name": "setTimeout", + "params": { + "timeout": { + "constraints": { + "max": 1440, + "min": 1, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.silent/commands/setTimeout" + } + }, + "deviceId": "0", + "feature": "ventilation.quickmodes.silent", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "defaultRuntime": { + "type": "number", + "unit": "minutes", + "value": 30 + }, + "isActiveWritable": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-12-17T13:24:04.515Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.silent" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": true, + "name": "activate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.standby/commands/activate" + }, + "deactivate": { + "isExecutable": true, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.standby/commands/deactivate" + } + }, + "deviceId": "0", + "feature": "ventilation.quickmodes.standby", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-12-17T13:24:04.515Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.quickmodes.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.productIdentification", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "product": { + "type": "object", + "value": { + "busAddress": 0, + "busType": "OwnBus", + "productFamily": "B_00059_VP300", + "viessmannIdentificationNumber": "################" + } + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.productIdentification" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.messages.errors.raw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "entries": { + "type": "array", + "value": [] + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.messages.errors.raw" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": true, + "name": "activate", + "params": { + "begin": { + "constraints": { + "regEx": "^[\\d]{2}-[\\d]{2}$" + }, + "required": true, + "type": "string" + }, + "end": { + "constraints": { + "regEx": "^[\\d]{2}-[\\d]{2}$" + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.time.daylightSaving/commands/activate" + }, + "deactivate": { + "isExecutable": true, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.time.daylightSaving/commands/deactivate" + } + }, + "deviceId": "0", + "feature": "device.time.daylightSaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.time.daylightSaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.boiler.serial", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "################" + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.boiler.serial" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.device.variant", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "Vitopure350" + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.device.variant" + }, + { + "apiVersion": 1, + "commands": { + "setMode": { + "isExecutable": true, + "name": "setMode", + "params": { + "mode": { + "constraints": { + "enum": ["permanent", "ventilation", "sensorDriven"] + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.active/commands/setMode" + }, + "setModeContinuousSensorOverride": { + "isExecutable": false, + "name": "setModeContinuousSensorOverride", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.active/commands/setModeContinuousSensorOverride" + } + }, + "deviceId": "0", + "feature": "ventilation.operating.modes.active", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "sensorDriven" + } + }, + "timestamp": "2024-12-17T08:16:15.525Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.modes.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "ventilation.operating.state", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "demand": { + "type": "string", + "value": "unknown" + }, + "level": { + "type": "string", + "value": "unknown" + }, + "reason": { + "type": "string", + "value": "standby" + } + }, + "timestamp": "2024-12-17T13:24:04.515Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.operating.state" + }, + { + "apiVersion": 1, + "commands": { + "resetSchedule": { + "isExecutable": false, + "name": "resetSchedule", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.schedule/commands/resetSchedule" + }, + "setSchedule": { + "isExecutable": true, + "name": "setSchedule", + "params": { + "newSchedule": { + "constraints": { + "defaultMode": "standby", + "maxEntries": 4, + "modes": ["levelOne", "levelTwo", "levelThree", "levelFour"], + "overlapAllowed": false, + "resolution": 10 + }, + "required": true, + "type": "Schedule" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.schedule/commands/setSchedule" + } + }, + "deviceId": "0", + "feature": "ventilation.schedule", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "entries": { + "type": "Schedule", + "value": { + "fri": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ], + "mon": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ], + "sat": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ], + "sun": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ], + "thu": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ], + "tue": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ], + "wed": [ + { + "end": "22:00", + "mode": "levelTwo", + "position": 0, + "start": "06:00" + } + ] + } + } + }, + "timestamp": "2024-12-17T07:50:28.062Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/ventilation.schedule" + } + ] +} diff --git a/tests/components/vicare/fixtures/Vitocal250A.json b/tests/components/vicare/fixtures/Vitocal250A.json new file mode 100644 index 00000000000..1da43531a89 --- /dev/null +++ b/tests/components/vicare/fixtures/Vitocal250A.json @@ -0,0 +1,4447 @@ +{ + "data": [ + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.messages.errors.raw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "entries": { + "type": "array", + "value": [] + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.messages.errors.raw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.productIdentification", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "product": { + "type": "object", + "value": { + "busAddress": 1, + "busType": "CanExternal", + "productFamily": "B_00027_VC250", + "viessmannIdentificationNumber": "################" + } + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.productIdentification" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.productMatrix", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "product": { + "type": "array", + "value": [ + { + "busAddress": 1, + "busType": "CanExternal", + "productFamily": "B_00027_VC250", + "viessmannIdentificationNumber": "################" + }, + { + "busAddress": 71, + "busType": "CanExternal", + "productFamily": "B_00012_VCH200", + "viessmannIdentificationNumber": "################" + } + ] + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.productMatrix" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "device.serial", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "deviceSerialVitocal250A" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.serial" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.boiler.sensors.temperature.commonSupply", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 44.6 + } + }, + "timestamp": "2024-10-01T16:28:33.694Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.boiler.sensors.temperature.commonSupply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.boiler.serial", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "################" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.boiler.serial" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.bufferCylinder.sensors.temperature.main", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.buffer.sensors.temperature.main", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 35.3 + } + }, + "timestamp": "2024-10-01T16:28:33.694Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.buffer.sensors.temperature.main" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.bufferCylinder.sensors.temperature.main", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 35.3 + } + }, + "timestamp": "2024-10-01T16:28:33.694Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.bufferCylinder.sensors.temperature.main" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "enabled": { + "type": "array", + "value": ["1"] + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.circulation.pump", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:09:57.180Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.circulation.pump" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.frostprotection", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.heating.curve", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.heating.curve" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.heating.schedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.heating.schedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.modes.active", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.modes.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.modes.cooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.modes.cooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.modes.heating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.modes.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.modes.heatingCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.modes.heatingCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.modes.standby", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.modes.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.active", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.comfortCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.comfortCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.comfortCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.comfortCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.comfortEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.comfortEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.comfortHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.comfortHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.eco", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.eco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.fixed", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.fixed" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.forcedLastFromSchedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.forcedLastFromSchedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.frostprotection", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.normalCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.normalCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.normalCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.normalCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.normalEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.normalEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.normalHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.normalHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.reducedCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.reducedCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.reducedCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.reducedCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.reducedEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.reducedEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.reducedHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.reducedHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.standby", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.circuits.0.operating.programs.summerEco", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.operating.programs.summerEco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.remoteController", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.remoteController" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.sensors.temperature.room", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.sensors.temperature.room" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.sensors.temperature.supply", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.sensors.temperature.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.temperature", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.temperature" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.temperature.levels", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.temperature.levels" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.0.zone.mode", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.0.zone.mode" + }, + { + "apiVersion": 1, + "commands": { + "setName": { + "isExecutable": true, + "name": "setName", + "params": { + "name": { + "constraints": { + "maxLength": 20, + "minLength": 1 + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1/commands/setName" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "name": { + "type": "string", + "value": "Heizkreis" + }, + "type": { + "type": "string", + "value": "heatingCircuit" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.circulation.pump", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "on" + } + }, + "timestamp": "2024-10-01T16:09:57.180Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.circulation.pump" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.frostprotection", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "off" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.frostprotection" + }, + { + "apiVersion": 1, + "commands": { + "setCurve": { + "isExecutable": true, + "name": "setCurve", + "params": { + "shift": { + "constraints": { + "max": 40, + "min": -13, + "stepping": 1 + }, + "required": true, + "type": "number" + }, + "slope": { + "constraints": { + "max": 3.5, + "min": 0.2, + "stepping": 0.1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.heating.curve/commands/setCurve" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.heating.curve", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "shift": { + "type": "number", + "unit": "", + "value": 0 + }, + "slope": { + "type": "number", + "unit": "", + "value": 1.1 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.heating.curve" + }, + { + "apiVersion": 1, + "commands": { + "resetSchedule": { + "isExecutable": true, + "name": "resetSchedule", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.heating.schedule/commands/resetSchedule" + }, + "setSchedule": { + "isExecutable": true, + "name": "setSchedule", + "params": { + "newSchedule": { + "constraints": { + "defaultMode": "reduced", + "maxEntries": 4, + "modes": ["normal", "comfort"], + "overlapAllowed": false, + "resolution": 10 + }, + "required": true, + "type": "Schedule" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.heating.schedule/commands/setSchedule" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.heating.schedule", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "entries": { + "type": "Schedule", + "value": { + "fri": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ], + "mon": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ], + "sat": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ], + "sun": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ], + "thu": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ], + "tue": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ], + "wed": [ + { + "end": "22:00", + "mode": "comfort", + "position": 0, + "start": "06:00" + } + ] + } + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.heating.schedule" + }, + { + "apiVersion": 1, + "commands": { + "setName": { + "isExecutable": true, + "name": "setName", + "params": { + "name": { + "constraints": { + "maxLength": 20, + "minLength": 1 + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.name/commands/setName" + } + }, + "components": [], + "deviceId": "0", + "feature": "heating.circuits.1.name", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "name": { + "type": "string", + "value": "Heizkreis" + } + }, + "timestamp": "2024-09-20T08:56:49.795Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.name" + }, + { + "apiVersion": 1, + "commands": { + "setMode": { + "isExecutable": true, + "name": "setMode", + "params": { + "mode": { + "constraints": { + "enum": ["heating", "standby"] + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.modes.active/commands/setMode" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.operating.modes.active", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "heating" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.modes.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.modes.cooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.modes.cooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.modes.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.modes.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.modes.heatingCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.modes.heatingCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.modes.standby", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.modes.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.active", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "comfortHeating" + } + }, + "timestamp": "2024-10-01T03:59:26.407Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.comfortCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.comfortCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "cooling" + }, + "reason": { + "type": "string", + "value": "eco" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.comfortEnergySaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "heating" + }, + "reason": { + "type": "string", + "value": "eco" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortEnergySaving" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": false, + "name": "activate", + "params": { + "temperature": { + "constraints": { + "max": 37, + "min": 3, + "stepping": 1 + }, + "required": false, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortHeating/commands/activate" + }, + "deactivate": { + "isExecutable": false, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortHeating/commands/deactivate" + }, + "setTemperature": { + "isExecutable": true, + "name": "setTemperature", + "params": { + "targetTemperature": { + "constraints": { + "max": 37, + "min": 3, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortHeating/commands/setTemperature" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.comfortHeating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "demand": { + "type": "string", + "value": "heating" + }, + "temperature": { + "type": "number", + "unit": "celsius", + "value": 24 + } + }, + "timestamp": "2024-10-01T03:59:26.407Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.comfortHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.eco", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T03:59:26.407Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.eco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.fixed", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.fixed" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": true, + "name": "activate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.forcedLastFromSchedule/commands/activate" + }, + "deactivate": { + "isExecutable": true, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.forcedLastFromSchedule/commands/deactivate" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.forcedLastFromSchedule", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.forcedLastFromSchedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.frostprotection", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.normalCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.normalCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "cooling" + }, + "reason": { + "type": "string", + "value": "eco" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.normalEnergySaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "heating" + }, + "reason": { + "type": "string", + "value": "eco" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalEnergySaving" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": false, + "name": "activate", + "params": { + "temperature": { + "constraints": { + "max": 37, + "min": 3, + "stepping": 1 + }, + "required": false, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalHeating/commands/activate" + }, + "deactivate": { + "isExecutable": false, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalHeating/commands/deactivate" + }, + "setTemperature": { + "isExecutable": true, + "name": "setTemperature", + "params": { + "targetTemperature": { + "constraints": { + "max": 37, + "min": 3, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalHeating/commands/setTemperature" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.normalHeating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "heating" + }, + "temperature": { + "type": "number", + "unit": "celsius", + "value": 24 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.normalHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.reducedCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.reducedCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "cooling" + }, + "reason": { + "type": "string", + "value": "eco" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.reducedEnergySaving", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "heating" + }, + "reason": { + "type": "string", + "value": "unknown" + } + }, + "timestamp": "2024-10-01T03:59:26.407Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedEnergySaving" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": false, + "name": "activate", + "params": { + "temperature": { + "constraints": { + "max": 37, + "min": 3, + "stepping": 1 + }, + "required": false, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedHeating/commands/activate" + }, + "deactivate": { + "isExecutable": false, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedHeating/commands/deactivate" + }, + "setTemperature": { + "isExecutable": true, + "name": "setTemperature", + "params": { + "targetTemperature": { + "constraints": { + "max": 37, + "min": 3, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedHeating/commands/setTemperature" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.reducedHeating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "demand": { + "type": "string", + "value": "heating" + }, + "temperature": { + "type": "number", + "unit": "celsius", + "value": 24 + } + }, + "timestamp": "2024-10-01T03:59:26.407Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.reducedHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.standby", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.circuits.1.operating.programs.summerEco", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T03:59:26.407Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.operating.programs.summerEco" + }, + { + "apiVersion": 1, + "commands": { + "removeZigbeeController": { + "isExecutable": false, + "name": "removeZigbeeController", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.remoteController/commands/removeZigbeeController" + }, + "setZigbeeController": { + "isExecutable": true, + "name": "setZigbeeController", + "params": { + "deviceId": { + "constraints": { + "enum": [] + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.remoteController/commands/setZigbeeController" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.remoteController", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.remoteController" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.sensors.temperature.room", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 24.1 + } + }, + "timestamp": "2024-10-01T16:05:52.313Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.sensors.temperature.room" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.sensors.temperature.supply", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 39 + } + }, + "timestamp": "2024-10-01T16:28:40.965Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.sensors.temperature.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.temperature", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:26:48.295Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.temperature" + }, + { + "apiVersion": 1, + "commands": { + "setLevels": { + "isExecutable": true, + "name": "setLevels", + "params": { + "maxTemperature": { + "constraints": { + "max": 70, + "min": 10, + "stepping": 1 + }, + "required": true, + "type": "number" + }, + "minTemperature": { + "constraints": { + "max": 30, + "min": 1, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.temperature.levels/commands/setLevels" + }, + "setMax": { + "isExecutable": true, + "name": "setMax", + "params": { + "temperature": { + "constraints": { + "max": 70, + "min": 10, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.temperature.levels/commands/setMax" + }, + "setMin": { + "isExecutable": true, + "name": "setMin", + "params": { + "temperature": { + "constraints": { + "max": 30, + "min": 1, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.temperature.levels/commands/setMin" + } + }, + "deviceId": "0", + "feature": "heating.circuits.1.temperature.levels", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "max": { + "type": "number", + "unit": "celsius", + "value": 55 + }, + "min": { + "type": "number", + "unit": "celsius", + "value": 20 + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.temperature.levels" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.1.zone.mode", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.1.zone.mode" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.circulation.pump", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:09:57.180Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.circulation.pump" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.frostprotection", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.heating.curve", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.heating.curve" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.heating.schedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.heating.schedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.modes.active", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.modes.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.modes.cooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.modes.cooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.modes.heating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.modes.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.modes.heatingCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.modes.heatingCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.modes.standby", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.modes.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.active", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.comfortCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.comfortCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.comfortCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.comfortCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.comfortEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.comfortEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.comfortHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.comfortHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.eco", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.eco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.fixed", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.fixed" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.forcedLastFromSchedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.forcedLastFromSchedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.frostprotection", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.normalCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.normalCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.normalCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.normalCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.normalEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.normalEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.normalHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.normalHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.reducedCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.reducedCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.reducedCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.reducedCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.reducedEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.reducedEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.reducedHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.reducedHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.standby", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.circuits.2.operating.programs.summerEco", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.operating.programs.summerEco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.remoteController", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.remoteController" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.sensors.temperature.room", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.sensors.temperature.room" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.sensors.temperature.supply", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.sensors.temperature.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.temperature", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.temperature" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.temperature.levels", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.temperature.levels" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.2.zone.mode", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.2.zone.mode" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.circulation.pump", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:09:57.180Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.circulation.pump" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.frostprotection", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.heating.curve", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.heating.curve" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.heating.schedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.heating.schedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.modes.active", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.modes.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.modes.cooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.modes.cooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.modes.heating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.modes.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.modes.heatingCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.modes.heatingCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.modes.standby", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.modes.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.active", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.comfortCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.comfortCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.comfortCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.comfortCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.comfortEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.comfortEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.comfortHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.comfortHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.eco", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.eco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.fixed", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.fixed" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.forcedLastFromSchedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.forcedLastFromSchedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.frostprotection", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.frostprotection" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.normalCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.normalCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.normalCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.normalCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.normalEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.normalEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.normalHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.normalHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.reducedCooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.reducedCooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.reducedCoolingEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.reducedCoolingEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.reducedEnergySaving", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.reducedEnergySaving" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.reducedHeating", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.reducedHeating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.standby", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.standby" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.circuits.N.operating.programs.reducedEnergySaving and heating.circuits.0.operating.programs.eco", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.circuits.3.operating.programs.summerEco", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.operating.programs.summerEco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.remoteController", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.remoteController" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.sensors.temperature.room", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.sensors.temperature.room" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.sensors.temperature.supply", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.sensors.temperature.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.temperature", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.temperature" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.temperature.levels", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.temperature.levels" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.circuits.3.zone.mode", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.circuits.3.zone.mode" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "enabled": { + "type": "array", + "value": ["0"] + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "phase": { + "type": "string", + "value": "ready" + } + }, + "timestamp": "2024-10-01T16:12:14.713Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.heat.production.current", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "watt", + "value": 13.317 + } + }, + "timestamp": "2024-10-01T16:28:29.219Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.heat.production.current" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.power.consumption.cooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.power.consumption.cooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.power.consumption.current", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "kilowatt", + "value": 3.107 + } + }, + "timestamp": "2024-10-01T16:28:29.219Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.power.consumption.current" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.power.consumption.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [7.6, 5.4, 3, 2.6, 4.3, 1.2, 4.2, 2.7] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T11:46:35.700Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [7.6, 93.9, 41.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T11:46:35.768Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [13, 21.799999999999997, 20.5, 27.4, 16.2] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T11:46:35.700Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [143, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T11:45:28.937Z" + } + }, + "timestamp": "2024-10-01T12:18:26.686Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.power.consumption.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.power.consumption.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [16.4, 31.2, 0, 0, 0, 0, 0, 0] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T16:25:33.871Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [16.4, 36.7, 2.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T16:25:33.871Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [47.599999999999994, 0, 0, 5.5, 0] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T16:25:33.871Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [55.2, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T16:25:33.871Z" + } + }, + "timestamp": "2024-10-01T16:27:05.568Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.power.consumption.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.power.consumption.total", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [24, 36.6, 3, 2.6, 4.3, 1.2, 4.2, 2.7] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [24, 130.60000000000002, 43.6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [60.599999999999994, 21.799999999999997, 20.5, 32.9, 16.2] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [198.2, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + } + }, + "timestamp": "2024-10-01T16:27:05.568Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.power.consumption.total" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.compressors.0.statistics", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "hours": { + "type": "number", + "unit": "hour", + "value": 71 + }, + "starts": { + "type": "number", + "unit": "", + "value": 121 + } + }, + "timestamp": "2024-10-01T16:12:54.682Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.compressors.0.statistics" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.device.variant", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "Vitocal250A" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.device.variant" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "status": { + "type": "string", + "value": "on" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": false, + "name": "activate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.hygiene/commands/activate" + }, + "disable": { + "isExecutable": false, + "name": "disable", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.hygiene/commands/disable" + }, + "enable": { + "isExecutable": true, + "name": "enable", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.hygiene/commands/enable" + } + }, + "deviceId": "0", + "feature": "heating.dhw.hygiene", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "enabled": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.hygiene" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.hygiene.trigger", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.hygiene.trigger" + }, + { + "apiVersion": 1, + "commands": { + "activate": { + "isExecutable": true, + "name": "activate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.oneTimeCharge/commands/activate" + }, + "deactivate": { + "isExecutable": true, + "name": "deactivate", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.oneTimeCharge/commands/deactivate" + } + }, + "deviceId": "0", + "feature": "heating.dhw.oneTimeCharge", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.oneTimeCharge" + }, + { + "apiVersion": 1, + "commands": { + "setMode": { + "isExecutable": true, + "name": "setMode", + "params": { + "mode": { + "constraints": { + "enum": ["efficientWithMinComfort", "efficient", "off"] + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.active/commands/setMode" + } + }, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.active", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "string", + "value": "efficientWithMinComfort" + } + }, + "timestamp": "2024-10-01T00:31:26.139Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.active" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.balanced", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.balanced" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.comfort", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:26.139Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.comfort" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.eco", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:26.139Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.eco" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.efficient", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.efficient" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.efficientWithMinComfort", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.efficientWithMinComfort" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.operating.modes.off", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.operating.modes.off" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.pumps.circulation", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.pumps.circulation" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.pumps.circulation.schedule", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.pumps.circulation.schedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.pumps.secondary", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.pumps.secondary" + }, + { + "apiVersion": 1, + "commands": { + "resetSchedule": { + "isExecutable": true, + "name": "resetSchedule", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.schedule/commands/resetSchedule" + }, + "setSchedule": { + "isExecutable": true, + "name": "setSchedule", + "params": { + "newSchedule": { + "constraints": { + "defaultMode": "off", + "maxEntries": 4, + "modes": ["on"], + "overlapAllowed": false, + "resolution": 10 + }, + "required": true, + "type": "Schedule" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.schedule/commands/setSchedule" + } + }, + "deviceId": "0", + "feature": "heating.dhw.schedule", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": true + }, + "entries": { + "type": "Schedule", + "value": { + "fri": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ], + "mon": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ], + "sat": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ], + "sun": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ], + "thu": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ], + "tue": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ], + "wed": [ + { + "end": "22:00", + "mode": "on", + "position": 0, + "start": "08:00" + } + ] + } + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.schedule" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.sensors.temperature.dhwCylinder", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 58.8 + } + }, + "timestamp": "2024-10-01T16:28:40.965Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.sensors.temperature.dhwCylinder" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.sensors.temperature.dhwCylinder.middle", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.sensors.temperature.dhwCylinder.middle" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.sensors.temperature.dhwCylinder.top", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:28:40.965Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.sensors.temperature.dhwCylinder.top" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.dhw.sensors.temperature.hotWaterStorage", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 58.8 + } + }, + "timestamp": "2024-10-01T16:28:40.965Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.sensors.temperature.hotWaterStorage" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder.middle", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.dhw.sensors.temperature.hotWaterStorage.middle", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.sensors.temperature.hotWaterStorage.middle" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.dhw.sensors.temperature.dhwCylinder.top", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.dhw.sensors.temperature.hotWaterStorage.top", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:28:40.965Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.sensors.temperature.hotWaterStorage.top" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.temperature.hygiene", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.hygiene" + }, + { + "apiVersion": 1, + "commands": { + "setHysteresis": { + "isExecutable": true, + "name": "setHysteresis", + "params": { + "hysteresis": { + "constraints": { + "max": 10, + "min": 1, + "stepping": 0.5 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.hysteresis/commands/setHysteresis" + }, + "setHysteresisSwitchOffValue": { + "isExecutable": true, + "name": "setHysteresisSwitchOffValue", + "params": { + "hysteresis": { + "constraints": { + "max": 2.5, + "min": 0, + "stepping": 0.5 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.hysteresis/commands/setHysteresisSwitchOffValue" + }, + "setHysteresisSwitchOnValue": { + "isExecutable": true, + "name": "setHysteresisSwitchOnValue", + "params": { + "hysteresis": { + "constraints": { + "max": 10, + "min": 1, + "stepping": 0.5 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.hysteresis/commands/setHysteresisSwitchOnValue" + } + }, + "deviceId": "0", + "feature": "heating.dhw.temperature.hysteresis", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "switchOffValue": { + "type": "number", + "unit": "kelvin", + "value": 0 + }, + "switchOnValue": { + "type": "number", + "unit": "kelvin", + "value": 5 + }, + "value": { + "type": "number", + "unit": "kelvin", + "value": 5 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.hysteresis" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.dhw.temperature.levels", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "default": { + "type": "number", + "unit": "celsius", + "value": 50 + }, + "max": { + "type": "number", + "unit": "celsius", + "value": 10 + }, + "min": { + "type": "number", + "unit": "celsius", + "value": 10 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.levels" + }, + { + "apiVersion": 1, + "commands": { + "setTargetTemperature": { + "isExecutable": true, + "name": "setTargetTemperature", + "params": { + "temperature": { + "constraints": { + "efficientLowerBorder": 0, + "efficientUpperBorder": 55, + "max": 60, + "min": 10, + "stepping": 1 + }, + "required": true, + "type": "number" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.main/commands/setTargetTemperature" + } + }, + "deviceId": "0", + "feature": "heating.dhw.temperature.main", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "celsius", + "value": 47 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.dhw.temperature.main" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.heat.production.current", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "watt", + "value": 0 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.heat.production.current" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.power.consumption.current", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "watt", + "value": 0 + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.power.consumption.current" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.power.consumption.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0, 0, 0, 0] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.power.consumption.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.power.consumption.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0, 0, 0, 0] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + } + }, + "timestamp": "2024-10-01T00:31:26.139Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.power.consumption.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.power.consumption.summary.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "currentDay": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "currentMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "currentYear": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "lastMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "lastSevenDays": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "lastYear": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.power.consumption.summary.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.power.consumption.summary.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "currentDay": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "currentMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "currentYear": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "lastMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "lastSevenDays": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + }, + "lastYear": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + } + }, + "timestamp": "2024-10-01T00:31:26.139Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.power.consumption.summary.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.power.consumption.total", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0, 0, 0, 0] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0, 0, 0, 0] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [0, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + } + }, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.power.consumption.total" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.statistics", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "hours": { + "type": "number", + "unit": "hour", + "value": 0 + }, + "starts": { + "type": "number", + "unit": "", + "value": 0 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.statistics" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.heatingRod.status", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "level1": { + "type": "boolean", + "value": false + }, + "level2": { + "type": "boolean", + "value": false + }, + "level3": { + "type": "boolean", + "value": false + }, + "overall": { + "type": "boolean", + "value": false + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.heatingRod.status" + }, + { + "apiVersion": 1, + "commands": { + "changeEndDate": { + "isExecutable": false, + "name": "changeEndDate", + "params": { + "end": { + "constraints": { + "regEx": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$", + "sameDayAllowed": true + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holiday/commands/changeEndDate" + }, + "schedule": { + "isExecutable": true, + "name": "schedule", + "params": { + "end": { + "constraints": { + "regEx": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$", + "sameDayAllowed": true + }, + "required": true, + "type": "string" + }, + "start": { + "constraints": { + "regEx": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$" + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holiday/commands/schedule" + }, + "unschedule": { + "isExecutable": true, + "name": "unschedule", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holiday/commands/unschedule" + } + }, + "deviceId": "0", + "feature": "heating.operating.programs.holiday", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "end": { + "type": "string", + "value": "2000-01-01" + }, + "start": { + "type": "string", + "value": "2000-01-01" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holiday" + }, + { + "apiVersion": 1, + "commands": { + "changeEndDate": { + "isExecutable": false, + "name": "changeEndDate", + "params": { + "end": { + "constraints": { + "regEx": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$", + "sameDayAllowed": true + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holidayAtHome/commands/changeEndDate" + }, + "schedule": { + "isExecutable": true, + "name": "schedule", + "params": { + "end": { + "constraints": { + "regEx": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$", + "sameDayAllowed": true + }, + "required": true, + "type": "string" + }, + "start": { + "constraints": { + "regEx": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$" + }, + "required": true, + "type": "string" + } + }, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holidayAtHome/commands/schedule" + }, + "unschedule": { + "isExecutable": true, + "name": "unschedule", + "params": {}, + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holidayAtHome/commands/unschedule" + } + }, + "deviceId": "0", + "feature": "heating.operating.programs.holidayAtHome", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "active": { + "type": "boolean", + "value": false + }, + "end": { + "type": "string", + "value": "2000-01-01" + }, + "start": { + "type": "string", + "value": "2000-01-01" + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.operating.programs.holidayAtHome" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.cooling", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T00:31:26.264Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.cooling" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.current", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "kilowatt", + "value": 3.107 + } + }, + "timestamp": "2024-10-01T16:28:29.219Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.current" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [7.6, 5.4, 3, 2.6, 4.3, 1.2, 4.2, 2.7] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [7.6, 93.9, 41.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [13, 21.799999999999997, 20.5, 27.4, 16.2] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [143, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + } + }, + "timestamp": "2024-10-01T12:18:26.686Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [16.4, 31.2, 0, 0, 0, 0, 0, 0] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [16.4, 36.7, 2.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [47.599999999999994, 0, 0, 5.5, 0] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [55.2, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.552Z" + } + }, + "timestamp": "2024-10-01T16:27:05.568Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.summary.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "currentDay": { + "type": "number", + "unit": "kilowattHour", + "value": 7.6 + }, + "currentMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 7.6 + }, + "currentYear": { + "type": "number", + "unit": "kilowattHour", + "value": 143 + }, + "lastMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 93.9 + }, + "lastSevenDays": { + "type": "number", + "unit": "kilowattHour", + "value": 28.3 + }, + "lastYear": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + } + }, + "timestamp": "2024-10-01T11:46:54.639Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.summary.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.summary.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "currentDay": { + "type": "number", + "unit": "kilowattHour", + "value": 16.4 + }, + "currentMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 16.4 + }, + "currentYear": { + "type": "number", + "unit": "kilowattHour", + "value": 55.2 + }, + "lastMonth": { + "type": "number", + "unit": "kilowattHour", + "value": 36.7 + }, + "lastSevenDays": { + "type": "number", + "unit": "kilowattHour", + "value": 47.6 + }, + "lastYear": { + "type": "number", + "unit": "kilowattHour", + "value": 0 + } + }, + "timestamp": "2024-10-01T16:27:05.568Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.summary.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.power.consumption.total", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "day": { + "type": "array", + "unit": "kilowattHour", + "value": [24, 36.6, 3, 2.6, 4.3, 1.2, 4.2, 2.7] + }, + "dayValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + }, + "month": { + "type": "array", + "unit": "kilowattHour", + "value": [24, 130.60000000000002, 43.6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "monthValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + }, + "week": { + "type": "array", + "unit": "kilowattHour", + "value": [60.599999999999994, 21.799999999999997, 20.5, 32.9, 16.2] + }, + "weekValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + }, + "year": { + "type": "array", + "unit": "kilowattHour", + "value": [198.2, 0] + }, + "yearValueReadAt": { + "type": "string", + "value": "2024-10-01T00:31:23.543Z" + } + }, + "timestamp": "2024-10-01T16:27:05.568Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.power.consumption.total" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.primaryCircuit.sensors.temperature.supply", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 12.8 + } + }, + "timestamp": "2024-10-01T16:28:36.488Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.primaryCircuit.sensors.temperature.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.spf.dhw", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.scop.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "", + "value": 4.1 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.scop.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.spf.heating", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.scop.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "", + "value": 3.2 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.scop.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deprecated": { + "info": "replaced by heating.spf.total", + "removalDate": "2024-09-15" + }, + "deviceId": "0", + "feature": "heating.scop.total", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "", + "value": 3.9 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.scop.total" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.secondaryCircuit.sensors.temperature.supply", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 45.1 + } + }, + "timestamp": "2024-10-01T16:28:36.488Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.secondaryCircuit.sensors.temperature.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.sensors.pressure.supply", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "bar", + "value": 2.1 + } + }, + "timestamp": "2024-10-01T15:06:07.125Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.sensors.pressure.supply" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.sensors.temperature.allengra", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 35.8 + } + }, + "timestamp": "2024-10-01T16:28:20.497Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.sensors.temperature.allengra" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.sensors.temperature.hydraulicSeparator", + "gatewayId": "################", + "isEnabled": false, + "isReady": true, + "properties": {}, + "timestamp": "2024-10-01T16:28:33.694Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.sensors.temperature.hydraulicSeparator" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.sensors.temperature.outside", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 14.3 + } + }, + "timestamp": "2024-10-01T16:28:36.488Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.sensors.temperature.outside" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.sensors.temperature.return", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "celsius", + "value": 35.3 + } + }, + "timestamp": "2024-10-01T16:28:04.882Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.sensors.temperature.return" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.sensors.volumetricFlow.allengra", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "status": { + "type": "string", + "value": "connected" + }, + "value": { + "type": "number", + "unit": "liter/hour", + "value": 1015 + } + }, + "timestamp": "2024-10-01T16:28:36.488Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.sensors.volumetricFlow.allengra" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.spf.dhw", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "", + "value": 4.1 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.spf.dhw" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.spf.heating", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "", + "value": 3.2 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.spf.heating" + }, + { + "apiVersion": 1, + "commands": {}, + "deviceId": "0", + "feature": "heating.spf.total", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "value": { + "type": "number", + "unit": "", + "value": 3.9 + } + }, + "timestamp": "2024-10-01T00:31:21.381Z", + "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/heating.spf.total" + } + ] +} diff --git a/tests/components/vicare/snapshots/test_binary_sensor.ambr b/tests/components/vicare/snapshots/test_binary_sensor.ambr index f3e4d4e1c84..93e407ea505 100644 --- a/tests/components/vicare/snapshots/test_binary_sensor.ambr +++ b/tests/components/vicare/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -334,6 +341,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -373,6 +381,54 @@ 'state': 'unavailable', }) # --- +# name: test_all_entities[binary_sensor.model0_one_time_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.model0_one_time_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'One-time charge', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'one_time_charge', + 'unique_id': 'gateway0_deviceSerialVitodens300W-one_time_charge', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.model0_one_time_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'model0 One-time charge', + }), + 'context': , + 'entity_id': 'binary_sensor.model0_one_time_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_binary_sensors[burner] StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/vicare/snapshots/test_button.ambr b/tests/components/vicare/snapshots/test_button.ambr index 9fadc6a983f..17dfc29e96e 100644 --- a/tests/components/vicare/snapshots/test_button.ambr +++ b/tests/components/vicare/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/vicare/snapshots/test_climate.ambr b/tests/components/vicare/snapshots/test_climate.ambr index aea0ea879c2..e1709acea42 100644 --- a/tests/components/vicare/snapshots/test_climate.ambr +++ b/tests/components/vicare/snapshots/test_climate.ambr @@ -18,6 +18,7 @@ 'target_temp_step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -101,6 +102,7 @@ 'target_temp_step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/vicare/snapshots/test_fan.ambr b/tests/components/vicare/snapshots/test_fan.ambr index 3ecc4277fd9..0bac421e2c7 100644 --- a/tests/components/vicare/snapshots/test_fan.ambr +++ b/tests/components/vicare/snapshots/test_fan.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[fan.model0_ventilation-entry] +# name: test_all_entities[fan.model1_ventilation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -9,16 +9,16 @@ , , , - , ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'fan', 'entity_category': None, - 'entity_id': 'fan.model0_ventilation', + 'entity_id': 'fan.model1_ventilation', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -29,21 +29,21 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan', + 'original_icon': 'mdi:fan-off', 'original_name': 'Ventilation', 'platform': 'vicare', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'ventilation', - 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation', + 'unique_id': 'gateway1_deviceId1-ventilation', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[fan.model0_ventilation-state] +# name: test_all_entities[fan.model1_ventilation-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'model0 Ventilation', - 'icon': 'mdi:fan', + 'friendly_name': 'model1 Ventilation', + 'icon': 'mdi:fan-off', 'percentage': 0, 'percentage_step': 25.0, 'preset_mode': None, @@ -51,15 +51,14 @@ , , , - , ]), - 'supported_features': , + 'supported_features': , }), 'context': , - 'entity_id': 'fan.model0_ventilation', + 'entity_id': 'fan.model1_ventilation', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'off', }) # --- diff --git a/tests/components/vicare/snapshots/test_number.ambr b/tests/components/vicare/snapshots/test_number.ambr index 5a030fc0213..b26d2d33590 100644 --- a/tests/components/vicare/snapshots/test_number.ambr +++ b/tests/components/vicare/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -68,6 +69,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -113,6 +115,64 @@ 'state': 'unavailable', }) # --- +# name: test_all_entities[number.model0_dhw_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.model0_dhw_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dhw_temperature', + 'unique_id': 'gateway0_deviceSerialVitodens300W-dhw_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[number.model0_dhw_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 DHW temperature', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.model0_dhw_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_all_entities[number.model0_heating_curve_shift-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -125,6 +185,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -182,6 +243,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -239,6 +301,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -294,6 +357,7 @@ 'step': 0.1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -349,6 +413,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -406,6 +471,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -463,6 +529,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -520,6 +587,7 @@ 'step': 1.0, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -565,60 +633,3 @@ 'state': 'unavailable', }) # --- -# name: test_all_entities[number.model0_dhw_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1, - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.model0_dhw_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'DHW temperature', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'dhw_temperature', - 'unique_id': 'gateway0_deviceSerialVitodens300W-dhw_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[number.model0_dhw_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'model0 DHW temperature', - 'max': 100.0, - 'min': 0.0, - 'mode': , - 'step': 1, - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'number.model0_dhw_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- diff --git a/tests/components/vicare/snapshots/test_sensor.ambr b/tests/components/vicare/snapshots/test_sensor.ambr index 793f3e87611..a0d4bf374c8 100644 --- a/tests/components/vicare/snapshots/test_sensor.ambr +++ b/tests/components/vicare/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[sensor.model0_boiler_temperature-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_boiler_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -34,7 +35,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_boiler_temperature-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_boiler_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -50,7 +51,7 @@ 'state': '63', }) # --- -# name: test_all_entities[sensor.model0_burner_hours-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_burner_hours-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -85,7 +87,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_burner_hours-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_burner_hours-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 Burner hours', @@ -100,7 +102,7 @@ 'state': '18726.3', }) # --- -# name: test_all_entities[sensor.model0_burner_modulation-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_burner_modulation-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -109,6 +111,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -135,7 +138,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_all_entities[sensor.model0_burner_modulation-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_burner_modulation-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 Burner modulation', @@ -150,7 +153,7 @@ 'state': '0', }) # --- -# name: test_all_entities[sensor.model0_burner_starts-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_burner_starts-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -159,6 +162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -185,7 +189,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.model0_burner_starts-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_burner_starts-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 Burner starts', @@ -199,7 +203,7 @@ 'state': '14315', }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_this_month-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_this_month-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -208,6 +212,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -234,7 +239,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_this_month-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_this_month-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 DHW gas consumption this month', @@ -248,7 +253,7 @@ 'state': '805', }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_this_week-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_this_week-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -257,6 +262,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -283,7 +289,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_this_week-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_this_week-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 DHW gas consumption this week', @@ -297,7 +303,7 @@ 'state': '84', }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_this_year-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_this_year-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -306,6 +312,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -332,7 +339,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_this_year-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_this_year-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 DHW gas consumption this year', @@ -346,7 +353,7 @@ 'state': '8203', }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_today-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_today-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -355,6 +362,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -381,7 +389,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_all_entities[sensor.model0_dhw_gas_consumption_today-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_gas_consumption_today-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 DHW gas consumption today', @@ -395,7 +403,7 @@ 'state': '22', }) # --- -# name: test_all_entities[sensor.model0_dhw_max_temperature-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_max_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -404,6 +412,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -430,7 +439,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_dhw_max_temperature-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_max_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -446,7 +455,7 @@ 'state': '60', }) # --- -# name: test_all_entities[sensor.model0_dhw_min_temperature-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_min_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -455,6 +464,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -481,7 +491,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_dhw_min_temperature-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_dhw_min_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -497,407 +507,7 @@ 'state': '10', }) # --- -# name: test_all_entities[sensor.model0_energy-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power consumption this month', - 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this month', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.model0_energy-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'model0 Energy', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.model0_energy', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '7.843', - }) -# --- -# name: test_all_entities[sensor.model0_electricity_consumption_this_year-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_electricity_consumption_this_year', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Electricity consumption this year', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power_consumption_this_year', - 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this year', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.model0_electricity_consumption_this_year-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'model0 Electricity consumption this year', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.model0_electricity_consumption_this_year', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '207.106', - }) -# --- -# name: test_all_entities[sensor.model0_electricity_consumption_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_electricity_consumption_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Electricity consumption today', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'power_consumption_today', - 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption today', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.model0_electricity_consumption_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'model0 Electricity consumption today', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.model0_electricity_consumption_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.219', - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_this_month-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_heating_gas_consumption_this_month', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Heating gas consumption this month', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'gas_consumption_heating_this_month', - 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_month', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_this_month-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'model0 Heating gas consumption this month', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.model0_heating_gas_consumption_this_month', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_this_week-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_heating_gas_consumption_this_week', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Heating gas consumption this week', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'gas_consumption_heating_this_week', - 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_week', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_this_week-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'model0 Heating gas consumption this week', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.model0_heating_gas_consumption_this_week', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_this_year-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_heating_gas_consumption_this_year', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Heating gas consumption this year', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'gas_consumption_heating_this_year', - 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_year', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_this_year-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'model0 Heating gas consumption this year', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.model0_heating_gas_consumption_this_year', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '30946', - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_today-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_heating_gas_consumption_today', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Heating gas consumption today', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'gas_consumption_heating_today', - 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_today', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.model0_heating_gas_consumption_today-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'model0 Heating gas consumption today', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.model0_heating_gas_consumption_today', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_all_entities[sensor.model0_outside_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.model0_outside_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Outside temperature', - 'platform': 'vicare', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'outside_temperature', - 'unique_id': 'gateway0_deviceSerialVitodens300W-outside_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[sensor.model0_outside_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'model0 Outside temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.model0_outside_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '20.8', - }) -# --- -# name: test_all_entities[sensor.model0_electricity_consumption_this_week-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_electricity_consumption_this_week-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -906,6 +516,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -932,7 +543,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_electricity_consumption_this_week-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_electricity_consumption_this_week-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -948,7 +559,363 @@ 'state': '0.829', }) # --- -# name: test_all_entities[sensor.model0_supply_temperature-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_electricity_consumption_this_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_electricity_consumption_this_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Electricity consumption this year', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_consumption_this_year', + 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_electricity_consumption_this_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Electricity consumption this year', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_electricity_consumption_this_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '207.106', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_electricity_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_electricity_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Electricity consumption today', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_consumption_today', + 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_electricity_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Electricity consumption today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_electricity_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.219', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power consumption this month', + 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this month', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.843', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_this_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_gas_consumption_this_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating gas consumption this month', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_consumption_heating_this_month', + 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_month', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_this_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Heating gas consumption this month', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_gas_consumption_this_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_this_week-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_gas_consumption_this_week', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating gas consumption this week', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_consumption_heating_this_week', + 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_week', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_this_week-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Heating gas consumption this week', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_gas_consumption_this_week', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_this_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_gas_consumption_this_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating gas consumption this year', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_consumption_heating_this_year', + 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_year', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_this_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Heating gas consumption this year', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_gas_consumption_this_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30946', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_gas_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating gas consumption today', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'gas_consumption_heating_today', + 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_today', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_heating_gas_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Heating gas consumption today', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_gas_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_outside_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -957,6 +924,59 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outside temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outside_temperature', + 'unique_id': 'gateway0_deviceSerialVitodens300W-outside_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Outside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.8', + }) +# --- +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_supply_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -983,7 +1003,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_supply_temperature-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_supply_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -999,7 +1019,7 @@ 'state': '63', }) # --- -# name: test_all_entities[sensor.model0_supply_temperature_2-entry] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_supply_temperature_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1008,6 +1028,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1034,7 +1055,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.model0_supply_temperature_2-state] +# name: test_all_entities[type:boiler-vicare/Vitodens300W.json][sensor.model0_supply_temperature_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', @@ -1050,6 +1071,1623 @@ 'state': '25.5', }) # --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_buffer_main_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_buffer_main_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Buffer main temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'buffer_main_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-buffer main temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_buffer_main_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Buffer main temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_buffer_main_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.3', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_compressor_hours-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_compressor_hours', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Compressor hours', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'compressor_hours', + 'unique_id': 'gateway0_deviceSerialVitocal250A-compressor_hours-0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_compressor_hours-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Compressor hours', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_compressor_hours', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '71', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_compressor_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_compressor_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Compressor phase', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'compressor_phase', + 'unique_id': 'gateway0_deviceSerialVitocal250A-compressor_phase-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_compressor_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Compressor phase', + }), + 'context': , + 'entity_id': 'sensor.model0_compressor_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ready', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_compressor_starts-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_compressor_starts', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Compressor starts', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'compressor_starts', + 'unique_id': 'gateway0_deviceSerialVitocal250A-compressor_starts-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_compressor_starts-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Compressor starts', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_compressor_starts', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '121', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_last_seven_days-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_electricity_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW electricity consumption last seven days', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_summary_dhw_consumption_heating_lastsevendays', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_dhw_consumption_heating_lastsevendays', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_last_seven_days-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 DHW electricity consumption last seven days', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_electricity_consumption_last_seven_days', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.3', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_this_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_electricity_consumption_this_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW electricity consumption this month', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_dhw_summary_consumption_heating_currentmonth', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_dhw_summary_consumption_heating_currentmonth', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_this_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 DHW electricity consumption this month', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_electricity_consumption_this_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.6', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_this_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_electricity_consumption_this_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW electricity consumption this year', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_dhw_summary_consumption_heating_currentyear', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_dhw_summary_consumption_heating_currentyear', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_this_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 DHW electricity consumption this year', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_electricity_consumption_this_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '143', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_electricity_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW electricity consumption today', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_dhw_summary_consumption_heating_currentday', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_dhw_summary_consumption_heating_currentday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_electricity_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 DHW electricity consumption today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_electricity_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7.6', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_max_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_max_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW max temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hotwater_max_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-hotwater_max_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_max_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 DHW max temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_max_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_min_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_min_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW min temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hotwater_min_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-hotwater_min_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_min_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 DHW min temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_min_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_storage_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_dhw_storage_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DHW storage temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dhw_storage_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-dhw_storage_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_dhw_storage_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 DHW storage temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_dhw_storage_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '58.8', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_electricity_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_electricity_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Electricity consumption today', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_consumption_today', + 'unique_id': 'gateway0_deviceSerialVitocal250A-power consumption today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_electricity_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Electricity consumption today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_electricity_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_last_seven_days-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_electricity_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating electricity consumption last seven days', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_summary_consumption_heating_lastsevendays', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_lastsevendays', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_last_seven_days-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Heating electricity consumption last seven days', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_electricity_consumption_last_seven_days', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '47.6', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_this_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_electricity_consumption_this_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating electricity consumption this month', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_summary_consumption_heating_currentmonth', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_currentmonth', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_this_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Heating electricity consumption this month', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_electricity_consumption_this_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.4', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_this_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_electricity_consumption_this_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating electricity consumption this year', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_summary_consumption_heating_currentyear', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_currentyear', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_this_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Heating electricity consumption this year', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_electricity_consumption_this_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '55.2', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_heating_electricity_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Heating electricity consumption today', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_summary_consumption_heating_currentday', + 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_currentday', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_electricity_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Heating electricity consumption today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_electricity_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '16.4', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_rod_hours-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_heating_rod_hours', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating rod hours', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_rod_hours', + 'unique_id': 'gateway0_deviceSerialVitocal250A-heating_rod_hours', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_rod_hours-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Heating rod hours', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_rod_hours', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_rod_starts-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_heating_rod_starts', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heating rod starts', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_rod_starts', + 'unique_id': 'gateway0_deviceSerialVitocal250A-heating_rod_starts', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_heating_rod_starts-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Heating rod starts', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_heating_rod_starts', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_outside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outside temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'outside_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-outside_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Outside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.3', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_primary_circuit_supply_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_primary_circuit_supply_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Primary circuit supply temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'primary_circuit_supply_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-primary_circuit_supply_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_primary_circuit_supply_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Primary circuit supply temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_primary_circuit_supply_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.8', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_return_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_return_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Return temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'return_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-return_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_return_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Return temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_return_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.3', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_seasonal_performance_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_seasonal_performance_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Seasonal performance factor', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spf_total', + 'unique_id': 'gateway0_deviceSerialVitocal250A-spf_total', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_seasonal_performance_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Seasonal performance factor', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_seasonal_performance_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.9', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_seasonal_performance_factor_domestic_hot_water-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_seasonal_performance_factor_domestic_hot_water', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Seasonal performance factor - domestic hot water', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spf_dhw', + 'unique_id': 'gateway0_deviceSerialVitocal250A-spf_dhw', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_seasonal_performance_factor_domestic_hot_water-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Seasonal performance factor - domestic hot water', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_seasonal_performance_factor_domestic_hot_water', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.1', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_seasonal_performance_factor_heating-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_seasonal_performance_factor_heating', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Seasonal performance factor - heating', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spf_heating', + 'unique_id': 'gateway0_deviceSerialVitocal250A-spf_heating', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_seasonal_performance_factor_heating-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Seasonal performance factor - heating', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_seasonal_performance_factor_heating', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.2', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_secondary_circuit_supply_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_secondary_circuit_supply_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Secondary circuit supply temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'secondary_circuit_supply_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-secondary_circuit_supply_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_secondary_circuit_supply_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Secondary circuit supply temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_secondary_circuit_supply_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45.1', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_supply_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_supply_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Supply pressure', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'supply_pressure', + 'unique_id': 'gateway0_deviceSerialVitocal250A-supply_pressure', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_supply_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'model0 Supply pressure', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.model0_supply_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.1', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_supply_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_supply_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Supply temperature', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'supply_temperature', + 'unique_id': 'gateway0_deviceSerialVitocal250A-supply_temperature-1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_supply_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'model0 Supply temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_supply_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '39', + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_volumetric_flow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_volumetric_flow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Volumetric flow', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'volumetric_flow', + 'unique_id': 'gateway0_deviceSerialVitocal250A-volumetric_flow', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_volumetric_flow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'model0 Volumetric flow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_volumetric_flow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.015', + }) +# --- +# name: test_all_entities[type:ventilation-vicare/ViAir300F.json][sensor.model0_ventilation_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'levelone', + 'leveltwo', + 'levelthree', + 'levelfour', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_ventilation_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Ventilation level', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ventilation_level', + 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:ventilation-vicare/ViAir300F.json][sensor.model0_ventilation_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'model0 Ventilation level', + 'options': list([ + 'standby', + 'levelone', + 'leveltwo', + 'levelthree', + 'levelfour', + ]), + }), + 'context': , + 'entity_id': 'sensor.model0_ventilation_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'levelone', + }) +# --- +# name: test_all_entities[type:ventilation-vicare/ViAir300F.json][sensor.model0_ventilation_reason-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'standby', + 'permanent', + 'schedule', + 'sensordriven', + 'silent', + 'forcedlevelfour', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_ventilation_reason', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Ventilation reason', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ventilation_reason', + 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation_reason', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[type:ventilation-vicare/ViAir300F.json][sensor.model0_ventilation_reason-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'model0 Ventilation reason', + 'options': list([ + 'standby', + 'permanent', + 'schedule', + 'sensordriven', + 'silent', + 'forcedlevelfour', + ]), + }), + 'context': , + 'entity_id': 'sensor.model0_ventilation_reason', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'permanent', + }) +# --- +# name: test_room_sensors[sensor.model0_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.model0_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'vicare', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'gateway0_zigbee_d87a3bfffe5d844a-battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_room_sensors[sensor.model0_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'model0 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.model0_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '89', + }) +# --- # name: test_room_sensors[sensor.model0_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1059,6 +2697,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1110,6 +2749,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1161,6 +2801,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1212,6 +2853,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/vicare/snapshots/test_water_heater.ambr b/tests/components/vicare/snapshots/test_water_heater.ambr index bca04b1bbfa..7b7ab91e086 100644 --- a/tests/components/vicare/snapshots/test_water_heater.ambr +++ b/tests/components/vicare/snapshots/test_water_heater.ambr @@ -9,6 +9,7 @@ 'min_temp': 10, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -65,6 +66,7 @@ 'min_temp': 10, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/vicare/test_binary_sensor.py b/tests/components/vicare/test_binary_sensor.py index b9b8a57a59b..44612673a11 100644 --- a/tests/components/vicare/test_binary_sensor.py +++ b/tests/components/vicare/test_binary_sensor.py @@ -43,7 +43,7 @@ async def test_all_entities( """Test all entities.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.BINARY_SENSOR]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/test_button.py b/tests/components/vicare/test_button.py index c024af41d78..cdc47e3833d 100644 --- a/tests/components/vicare/test_button.py +++ b/tests/components/vicare/test_button.py @@ -25,7 +25,7 @@ async def test_all_entities( """Test all entities.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.BUTTON]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/test_climate.py b/tests/components/vicare/test_climate.py index 44df87276e7..f48a8988cf0 100644 --- a/tests/components/vicare/test_climate.py +++ b/tests/components/vicare/test_climate.py @@ -25,7 +25,7 @@ async def test_all_entities( """Test all entities.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.CLIMATE]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index a522cf75d5d..ce3b3c27f06 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -9,12 +9,12 @@ from PyViCare.PyViCareUtils import ( ) from syrupy.assertion import SnapshotAssertion -from homeassistant.components import dhcp from homeassistant.components.vicare.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import MOCK_MAC, MODULE @@ -28,7 +28,7 @@ VALID_CONFIG = { CONF_CLIENT_ID: "5678", } -DHCP_INFO = dhcp.DhcpServiceInfo( +DHCP_INFO = DhcpServiceInfo( ip="1.1.1.1", hostname="mock_hostname", macaddress=MOCK_MAC.lower().replace(":", ""), @@ -49,7 +49,7 @@ async def test_user_create_entry( # test PyViCareInvalidConfigurationError with patch( - f"{MODULE}.config_flow.vicare_login", + f"{MODULE}.config_flow.login", side_effect=PyViCareInvalidConfigurationError( {"error": "foo", "error_description": "bar"} ), @@ -65,7 +65,7 @@ async def test_user_create_entry( # test PyViCareInvalidCredentialsError with patch( - f"{MODULE}.config_flow.vicare_login", + f"{MODULE}.config_flow.login", side_effect=PyViCareInvalidCredentialsError, ): result = await hass.config_entries.flow.async_configure( @@ -79,7 +79,7 @@ async def test_user_create_entry( # test success with patch( - f"{MODULE}.config_flow.vicare_login", + f"{MODULE}.config_flow.login", return_value=None, ): result = await hass.config_entries.flow.async_configure( @@ -110,7 +110,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> # test PyViCareInvalidConfigurationError with patch( - f"{MODULE}.config_flow.vicare_login", + f"{MODULE}.config_flow.login", side_effect=PyViCareInvalidConfigurationError( {"error": "foo", "error_description": "bar"} ), @@ -125,7 +125,7 @@ async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> # test success with patch( - f"{MODULE}.config_flow.vicare_login", + f"{MODULE}.config_flow.login", return_value=None, ): result = await hass.config_entries.flow.async_configure( @@ -160,7 +160,7 @@ async def test_form_dhcp( assert result["errors"] == {} with patch( - f"{MODULE}.config_flow.vicare_login", + f"{MODULE}.config_flow.login", return_value=None, ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/vicare/test_fan.py b/tests/components/vicare/test_fan.py index ba5db6e42c7..5683f48f01f 100644 --- a/tests/components/vicare/test_fan.py +++ b/tests/components/vicare/test_fan.py @@ -23,9 +23,12 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - fixtures: list[Fixture] = [Fixture({"type:ventilation"}, "vicare/ViAir300F.json")] + fixtures: list[Fixture] = [ + Fixture({"type:ventilation"}, "vicare/ViAir300F.json"), + Fixture({"type:ventilation"}, "vicare/VitoPure.json"), + ] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.FAN]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/test_init.py b/tests/components/vicare/test_init.py index 62bec7f50c5..d553f2758b8 100644 --- a/tests/components/vicare/test_init.py +++ b/tests/components/vicare/test_init.py @@ -26,7 +26,7 @@ async def test_device_and_entity_migration( Fixture({"type:boiler"}, "vicare/dummy-device-no-serial.json"), ] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.CLIMATE]), ): mock_config_entry.add_to_hass(hass) diff --git a/tests/components/vicare/test_number.py b/tests/components/vicare/test_number.py index c3aa66a86f6..7b9c1915b95 100644 --- a/tests/components/vicare/test_number.py +++ b/tests/components/vicare/test_number.py @@ -25,7 +25,7 @@ async def test_all_entities( """Test all entities.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.NUMBER]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/test_sensor.py b/tests/components/vicare/test_sensor.py index 06c8b963680..daad6bfa1c8 100644 --- a/tests/components/vicare/test_sensor.py +++ b/tests/components/vicare/test_sensor.py @@ -16,18 +16,28 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("fixture_type", "fixture_data"), + [ + ("type:boiler", "vicare/Vitodens300W.json"), + ("type:heatpump", "vicare/Vitocal250A.json"), + ("type:ventilation", "vicare/ViAir300F.json"), + ], +) async def test_all_entities( hass: HomeAssistant, + fixture_type: str, + fixture_data: str, snapshot: SnapshotAssertion, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" fixtures: list[Fixture] = [ - Fixture({"type:boiler"}, "vicare/Vitodens300W.json"), + Fixture({fixture_type}, fixture_data), ] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.SENSOR]), ): await setup_integration(hass, mock_config_entry) @@ -48,7 +58,7 @@ async def test_room_sensors( Fixture({"type:climateSensor"}, "vicare/RoomSensor2.json"), ] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.SENSOR]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vicare/test_utils.py b/tests/components/vicare/test_utils.py new file mode 100644 index 00000000000..13ca77f2792 --- /dev/null +++ b/tests/components/vicare/test_utils.py @@ -0,0 +1,23 @@ +"""Test ViCare utils.""" + +import pytest + +from homeassistant.components.vicare.utils import filter_state + + +@pytest.mark.parametrize( + ("state", "expected_result"), + [ + (None, None), + ("unknown", None), + ("nothing", None), + ("levelOne", "levelOne"), + ], +) +async def test_filter_state( + state: str | None, + expected_result: str | None, +) -> None: + """Test filter_state.""" + + assert filter_state(state) == expected_result diff --git a/tests/components/vicare/test_water_heater.py b/tests/components/vicare/test_water_heater.py index fbb5863cf7a..f9ca431af6d 100644 --- a/tests/components/vicare/test_water_heater.py +++ b/tests/components/vicare/test_water_heater.py @@ -25,7 +25,7 @@ async def test_all_entities( """Test all entities.""" fixtures: list[Fixture] = [Fixture({"type:boiler"}, "vicare/Vitodens300W.json")] with ( - patch(f"{MODULE}.vicare_login", return_value=MockPyViCare(fixtures)), + patch(f"{MODULE}.login", return_value=MockPyViCare(fixtures)), patch(f"{MODULE}.PLATFORMS", [Platform.WATER_HEATER]), ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 51151ae8f42..5fbf61a58da 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -2,7 +2,6 @@ from ipaddress import ip_address -from homeassistant.components import zeroconf from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, MediaPlayerDeviceClass, @@ -27,6 +26,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PIN, ) +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.util import slugify NAME = "Vizio" @@ -173,7 +173,7 @@ VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" ZEROCONF_HOST, ZEROCONF_PORT = HOST.split(":", maxsplit=2) -MOCK_ZEROCONF_SERVICE_INFO = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_SERVICE_INFO = ZeroconfServiceInfo( ip_address=ip_address(ZEROCONF_HOST), ip_addresses=[ip_address(ZEROCONF_HOST)], hostname="mock_hostname", diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index e004255ec6d..9d776ba6a59 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.vizio.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID diff --git a/tests/components/vodafone_station/__init__.py b/tests/components/vodafone_station/__init__.py index 68f11a27b95..6119d94c06c 100644 --- a/tests/components/vodafone_station/__init__.py +++ b/tests/components/vodafone_station/__init__.py @@ -1 +1,13 @@ """Tests for the Vodafone Station integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/vodafone_station/conftest.py b/tests/components/vodafone_station/conftest.py new file mode 100644 index 00000000000..a065a1e8065 --- /dev/null +++ b/tests/components/vodafone_station/conftest.py @@ -0,0 +1,86 @@ +"""Configure tests for Vodafone Station.""" + +from datetime import UTC, datetime + +from aiovodafone import VodafoneStationDevice +import pytest + +from homeassistant.components.vodafone_station import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +from .const import DEVICE_1_HOST, DEVICE_1_MAC, DEVICE_2_MAC + +from tests.common import ( + AsyncMock, + Generator, + MockConfigEntry, + load_json_object_fixture, + patch, +) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.vodafone_station.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_vodafone_station_router() -> Generator[AsyncMock]: + """Mock a Vodafone Station router.""" + with ( + patch( + "homeassistant.components.vodafone_station.coordinator.VodafoneStationSercommApi", + autospec=True, + ) as mock_router, + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi", + new=mock_router, + ), + ): + router = mock_router.return_value + router.get_devices_data.return_value = { + DEVICE_1_MAC: VodafoneStationDevice( + connected=True, + connection_type="wifi", + ip_address="192.168.1.10", + name=DEVICE_1_HOST, + mac=DEVICE_1_MAC, + type="laptop", + wifi="2.4G", + ), + DEVICE_2_MAC: VodafoneStationDevice( + connected=False, + connection_type="lan", + ip_address="192.168.1.11", + name="LanDevice1", + mac=DEVICE_2_MAC, + type="desktop", + wifi="", + ), + } + router.get_sensor_data.return_value = load_json_object_fixture( + "get_sensor_data.json", DOMAIN + ) + router.convert_uptime.return_value = datetime( + 2024, 11, 19, 20, 19, 0, tzinfo=UTC + ) + router.base_url = "https://fake_host" + yield router + + +@pytest.fixture +def mock_config_entry() -> Generator[MockConfigEntry]: + """Mock a Vodafone Station config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) diff --git a/tests/components/vodafone_station/const.py b/tests/components/vodafone_station/const.py index fc6bbd01398..cf6c274e5d5 100644 --- a/tests/components/vodafone_station/const.py +++ b/tests/components/vodafone_station/const.py @@ -1,117 +1,6 @@ """Common stuff for Vodafone Station tests.""" -from aiovodafone.api import VodafoneStationDevice - -from homeassistant.components.vodafone_station.const import DOMAIN -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME - -MOCK_CONFIG = { - DOMAIN: { - CONF_DEVICES: [ - { - CONF_HOST: "fake_host", - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - } - ] - } -} - -MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] - - -DEVICE_DATA_QUERY = { - "xx:xx:xx:xx:xx:xx": VodafoneStationDevice( - connected=True, - connection_type="wifi", - ip_address="192.168.1.10", - name="WifiDevice0", - mac="xx:xx:xx:xx:xx:xx", - type="laptop", - wifi="2.4G", - ), -} - -SERIAL = "m123456789" - -SENSOR_DATA_QUERY = { - "sys_serial_number": SERIAL, - "sys_firmware_version": "XF6_4.0.05.04", - "sys_bootloader_version": "0220", - "sys_hardware_version": "RHG3006 v1", - "omci_software_version": "\t\t1.0.0.1_41032\t\t\n", - "sys_uptime": "12:16:41", - "sys_cpu_usage": "97%", - "sys_reboot_cause": "Web Reboot", - "sys_memory_usage": "51.94%", - "sys_wireless_driver_version": "17.10.188.75;17.10.188.75", - "sys_wireless_driver_version_5g": "17.10.188.75;17.10.188.75", - "vf_internet_key_online_since": "", - "vf_internet_key_ip_addr": "0.0.0.0", - "vf_internet_key_system": "0.0.0.0", - "vf_internet_key_mode": "Auto", - "sys_voip_version": "v02.01.00_01.13a\n", - "sys_date_time": "20.10.2024 | 03:44 pm", - "sys_build_time": "Sun Jun 23 17:55:49 CST 2024\n", - "sys_model_name": "RHG3006", - "inter_ip_address": "1.1.1.1", - "inter_gateway": "1.1.1.2", - "inter_primary_dns": "1.1.1.3", - "inter_secondary_dns": "1.1.1.4", - "inter_firewall": "601036", - "inter_wan_ip_address": "1.1.1.1", - "inter_ipv6_link_local_address": "", - "inter_ipv6_link_global_address": "", - "inter_ipv6_gateway": "", - "inter_ipv6_prefix_delegation": "", - "inter_ipv6_dns_address1": "", - "inter_ipv6_dns_address2": "", - "lan_ip_network": "192.168.0.1/24", - "lan_default_gateway": "192.168.0.1", - "lan_subnet_address_subnet1": "", - "lan_mac_address": "11:22:33:44:55:66", - "lan_dhcp_server": "601036", - "lan_dhcpv6_server": "601036", - "lan_router_advertisement": "601036", - "lan_ipv6_default_gateway": "fe80::1", - "lan_port1_switch_mode": "1301722", - "lan_port2_switch_mode": "1301722", - "lan_port3_switch_mode": "1301722", - "lan_port4_switch_mode": "1301722", - "lan_port1_switch_speed": "10", - "lan_port2_switch_speed": "100", - "lan_port3_switch_speed": "1000", - "lan_port4_switch_speed": "1000", - "lan_port1_switch_status": "1301724", - "lan_port2_switch_status": "1301724", - "lan_port3_switch_status": "1301724", - "lan_port4_switch_status": "1301724", - "wifi_status": "601036", - "wifi_name": "Wifi-Main-Network", - "wifi_mac_address": "AA:BB:CC:DD:EE:FF", - "wifi_security": "401027", - "wifi_channel": "8", - "wifi_bandwidth": "573", - "guest_wifi_status": "601037", - "guest_wifi_name": "Wifi-Guest", - "guest_wifi_mac_addr": "AA:BB:CC:DD:EE:GG", - "guest_wifi_security": "401027", - "guest_wifi_channel": "N/A", - "guest_wifi_ip": "192.168.2.1", - "guest_wifi_subnet_addr": "255.255.255.0", - "guest_wifi_dhcp_server": "192.168.2.1", - "wifi_status_5g": "601036", - "wifi_name_5g": "Wifi-Main-Network", - "wifi_mac_address_5g": "AA:BB:CC:DD:EE:HH", - "wifi_security_5g": "401027", - "wifi_channel_5g": "36", - "wifi_bandwidth_5g": "4803", - "guest_wifi_status_5g": "601037", - "guest_wifi_name_5g": "Wifi-Guest", - "guest_wifi_mac_addr_5g": "AA:BB:CC:DD:EE:II", - "guest_wifi_channel_5g": "N/A", - "guest_wifi_security_5g": "401027", - "guest_wifi_ip_5g": "192.168.2.1", - "guest_wifi_subnet_addr_5g": "255.255.255.0", - "guest_wifi_dhcp_server_5g": "192.168.2.1", -} +DEVICE_1_HOST = "WifiDevice0" +DEVICE_1_MAC = "xx:xx:xx:xx:xx:xx" +DEVICE_2_HOST = "LanDevice1" +DEVICE_2_MAC = "yy:yy:yy:yy:yy:yy" diff --git a/tests/components/vodafone_station/fixtures/get_sensor_data.json b/tests/components/vodafone_station/fixtures/get_sensor_data.json new file mode 100644 index 00000000000..6a6229ebd18 --- /dev/null +++ b/tests/components/vodafone_station/fixtures/get_sensor_data.json @@ -0,0 +1,81 @@ +{ + "sys_serial_number": "m123456789", + "sys_firmware_version": "XF6_4.0.05.04", + "sys_bootloader_version": "0220", + "sys_hardware_version": "RHG3006 v1", + "omci_software_version": "\t\t1.0.0.1_41032\t\t\n", + "sys_uptime": "12:16:41", + "sys_cpu_usage": "97%", + "sys_reboot_cause": "Web Reboot", + "sys_memory_usage": "51.94%", + "sys_wireless_driver_version": "17.10.188.75;17.10.188.75", + "sys_wireless_driver_version_5g": "17.10.188.75;17.10.188.75", + "vf_internet_key_online_since": "", + "vf_internet_key_ip_addr": "0.0.0.0", + "vf_internet_key_system": "0.0.0.0", + "vf_internet_key_mode": "Auto", + "sys_voip_version": "v02.01.00_01.13a\n", + "sys_date_time": "20.10.2024 | 03:44 pm", + "sys_build_time": "Sun Jun 23 17:55:49 CST 2024\n", + "sys_model_name": "RHG3006", + "inter_ip_address": "1.1.1.1", + "inter_gateway": "1.1.1.2", + "inter_primary_dns": "1.1.1.3", + "inter_secondary_dns": "1.1.1.4", + "inter_firewall": "601036", + "inter_wan_ip_address": "1.1.1.1", + "inter_ipv6_link_local_address": "", + "inter_ipv6_link_global_address": "", + "inter_ipv6_gateway": "", + "inter_ipv6_prefix_delegation": "", + "inter_ipv6_dns_address1": "", + "inter_ipv6_dns_address2": "", + "lan_ip_network": "192.168.0.1/24", + "lan_default_gateway": "192.168.0.1", + "lan_subnet_address_subnet1": "", + "lan_mac_address": "11:22:33:44:55:66", + "lan_dhcp_server": "601036", + "lan_dhcpv6_server": "601036", + "lan_router_advertisement": "601036", + "lan_ipv6_default_gateway": "fe80::1", + "lan_port1_switch_mode": "1301722", + "lan_port2_switch_mode": "1301722", + "lan_port3_switch_mode": "1301722", + "lan_port4_switch_mode": "1301722", + "lan_port1_switch_speed": "10", + "lan_port2_switch_speed": "100", + "lan_port3_switch_speed": "1000", + "lan_port4_switch_speed": "1000", + "lan_port1_switch_status": "1301724", + "lan_port2_switch_status": "1301724", + "lan_port3_switch_status": "1301724", + "lan_port4_switch_status": "1301724", + "wifi_status": "601036", + "wifi_name": "Wifi-Main-Network", + "wifi_mac_address": "AA:BB:CC:DD:EE:FF", + "wifi_security": "401027", + "wifi_channel": "8", + "wifi_bandwidth": "573", + "guest_wifi_status": "601037", + "guest_wifi_name": "Wifi-Guest", + "guest_wifi_mac_addr": "AA:BB:CC:DD:EE:GG", + "guest_wifi_security": "401027", + "guest_wifi_channel": "N/A", + "guest_wifi_ip": "192.168.2.1", + "guest_wifi_subnet_addr": "255.255.255.0", + "guest_wifi_dhcp_server": "192.168.2.1", + "wifi_status_5g": "601036", + "wifi_name_5g": "Wifi-Main-Network", + "wifi_mac_address_5g": "AA:BB:CC:DD:EE:HH", + "wifi_security_5g": "401027", + "wifi_channel_5g": "36", + "wifi_bandwidth_5g": "4803", + "guest_wifi_status_5g": "601037", + "guest_wifi_name_5g": "Wifi-Guest", + "guest_wifi_mac_addr_5g": "AA:BB:CC:DD:EE:II", + "guest_wifi_channel_5g": "N/A", + "guest_wifi_security_5g": "401027", + "guest_wifi_ip_5g": "192.168.2.1", + "guest_wifi_subnet_addr_5g": "255.255.255.0", + "guest_wifi_dhcp_server_5g": "192.168.2.1" +} diff --git a/tests/components/vodafone_station/snapshots/test_button.ambr b/tests/components/vodafone_station/snapshots/test_button.ambr new file mode 100644 index 00000000000..736f590241a --- /dev/null +++ b/tests/components/vodafone_station/snapshots/test_button.ambr @@ -0,0 +1,49 @@ +# serializer version: 1 +# name: test_all_entities[button.vodafone_station_m123456789_restart-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vodafone_station_m123456789_restart', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Restart', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'm123456789_reboot', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.vodafone_station_m123456789_restart-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': 'Vodafone Station (m123456789) Restart', + }), + 'context': , + 'entity_id': 'button.vodafone_station_m123456789_restart', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/vodafone_station/snapshots/test_device_tracker.ambr b/tests/components/vodafone_station/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..7f98aad1405 --- /dev/null +++ b/tests/components/vodafone_station/snapshots/test_device_tracker.ambr @@ -0,0 +1,103 @@ +# serializer version: 1 +# name: test_all_entities[device_tracker.landevice1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.landevice1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'LanDevice1', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_tracker', + 'unique_id': 'yy:yy:yy:yy:yy:yy', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[device_tracker.landevice1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'LanDevice1', + 'host_name': 'LanDevice1', + 'ip': '192.168.1.11', + 'mac': 'yy:yy:yy:yy:yy:yy', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.landevice1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- +# name: test_all_entities[device_tracker.wifidevice0-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.wifidevice0', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'WifiDevice0', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'device_tracker', + 'unique_id': 'xx:xx:xx:xx:xx:xx', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[device_tracker.wifidevice0-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'WifiDevice0', + 'host_name': 'WifiDevice0', + 'ip': '192.168.1.10', + 'mac': 'xx:xx:xx:xx:xx:xx', + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.wifidevice0', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'home', + }) +# --- diff --git a/tests/components/vodafone_station/snapshots/test_diagnostics.ambr b/tests/components/vodafone_station/snapshots/test_diagnostics.ambr index dd268f4ed1a..be2956e0aab 100644 --- a/tests/components/vodafone_station/snapshots/test_diagnostics.ambr +++ b/tests/components/vodafone_station/snapshots/test_diagnostics.ambr @@ -9,6 +9,12 @@ 'hostname': 'WifiDevice0', 'type': 'laptop', }), + dict({ + 'connected': False, + 'connection_type': 'lan', + 'hostname': 'LanDevice1', + 'type': 'desktop', + }), ]), 'last_exception': None, 'last_update success': True, diff --git a/tests/components/vodafone_station/snapshots/test_sensor.ambr b/tests/components/vodafone_station/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..169ee92a24b --- /dev/null +++ b/tests/components/vodafone_station/snapshots/test_sensor.ambr @@ -0,0 +1,251 @@ +# serializer version: 1 +# name: test_all_entities[sensor.vodafone_station_m123456789_active_connection-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'dsl', + 'fiber', + 'internet_key', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.vodafone_station_m123456789_active_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active connection', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_connection', + 'unique_id': 'm123456789_inter_ip_address', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_active_connection-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Vodafone Station (m123456789) Active connection', + 'options': list([ + 'dsl', + 'fiber', + 'internet_key', + ]), + }), + 'context': , + 'entity_id': 'sensor.vodafone_station_m123456789_active_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_cpu_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.vodafone_station_m123456789_cpu_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CPU usage', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sys_cpu_usage', + 'unique_id': 'm123456789_sys_cpu_usage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_cpu_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vodafone Station (m123456789) CPU usage', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.vodafone_station_m123456789_cpu_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97.0', + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_memory_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.vodafone_station_m123456789_memory_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Memory usage', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sys_memory_usage', + 'unique_id': 'm123456789_sys_memory_usage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_memory_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vodafone Station (m123456789) Memory usage', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.vodafone_station_m123456789_memory_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.94', + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_reboot_cause-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.vodafone_station_m123456789_reboot_cause', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reboot cause', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sys_reboot_cause', + 'unique_id': 'm123456789_sys_reboot_cause', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_reboot_cause-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vodafone Station (m123456789) Reboot cause', + }), + 'context': , + 'entity_id': 'sensor.vodafone_station_m123456789_reboot_cause', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Web Reboot', + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.vodafone_station_m123456789_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'vodafone_station', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sys_uptime', + 'unique_id': 'm123456789_sys_uptime', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.vodafone_station_m123456789_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Vodafone Station (m123456789) Uptime', + }), + 'context': , + 'entity_id': 'sensor.vodafone_station_m123456789_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-11-19T20:19:00+00:00', + }) +# --- diff --git a/tests/components/vodafone_station/test_button.py b/tests/components/vodafone_station/test_button.py index 8b9b0753caa..d5f377d3f6f 100644 --- a/tests/components/vodafone_station/test_button.py +++ b/tests/components/vodafone_station/test_button.py @@ -1,56 +1,48 @@ """Tests for Vodafone Station button platform.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.components.vodafone_station.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.helpers import entity_registry as er -from .const import DEVICE_DATA_QUERY, MOCK_USER_DATA, SENSOR_DATA_QUERY, SERIAL +from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -async def test_button(hass: HomeAssistant, entity_registry: EntityRegistry) -> None: +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.vodafone_station.PLATFORMS", [Platform.BUTTON] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_pressing_button( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test device restart button.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - entry.add_to_hass(hass) + await setup_integration(hass, mock_config_entry) - with ( - patch("aiovodafone.api.VodafoneStationSercommApi.login"), - patch( - "aiovodafone.api.VodafoneStationSercommApi.get_devices_data", - return_value=DEVICE_DATA_QUERY, - ), - patch( - "aiovodafone.api.VodafoneStationSercommApi.get_sensor_data", - return_value=SENSOR_DATA_QUERY, - ), - patch( - "aiovodafone.api.VodafoneStationSercommApi.restart_router", - ) as mock_router_restart, - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - entity_id = f"button.vodafone_station_{SERIAL}_restart" - - # restart button - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_UNKNOWN - - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.unique_id == f"{SERIAL}_reboot" - - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - assert mock_router_restart.call_count == 1 + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.vodafone_station_m123456789_restart"}, + blocking=True, + ) + mock_vodafone_station_router.restart_router.assert_called_once() diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index 3a54f250871..68f8247bdf9 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -1,8 +1,13 @@ """Tests for Vodafone Station config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock -from aiovodafone import exceptions as aiovodafone_exceptions +from aiovodafone import ( + AlreadyLogged, + CannotAuthenticate, + CannotConnect, + ModelNotSupported, +) import pytest from homeassistant.components.device_tracker import CONF_CONSIDER_HOME @@ -12,39 +17,36 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import MOCK_USER_DATA - from tests.common import MockConfigEntry -async def test_user(hass: HomeAssistant) -> None: +async def test_user( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: """Test starting a flow by user.""" - with ( - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), - patch( - "homeassistant.components.vodafone_station.async_setup_entry" - ) as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_USERNAME] == "fake_username" - assert result["data"][CONF_PASSWORD] == "fake_password" - assert not result["result"].unique_id - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + } + assert not result["result"].unique_id assert mock_setup_entry.called @@ -52,14 +54,20 @@ async def test_user(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("side_effect", "error"), [ - (aiovodafone_exceptions.CannotConnect, "cannot_connect"), - (aiovodafone_exceptions.CannotAuthenticate, "invalid_auth"), - (aiovodafone_exceptions.AlreadyLogged, "already_logged"), - (aiovodafone_exceptions.ModelNotSupported, "model_not_supported"), + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (AlreadyLogged, "already_logged"), + (ModelNotSupported, "model_not_supported"), (ConnectionResetError, "unknown"), ], ) -async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> None: +async def test_exception_connection( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_setup_entry: AsyncMock, + side_effect: Exception, + error: str, +) -> None: """Test starting a flow by user with a connection error.""" result = await hass.config_entries.flow.async_init( @@ -68,178 +76,153 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" - with patch( - "aiovodafone.api.VodafoneStationSercommApi.login", - side_effect=side_effect, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_DATA - ) + mock_vodafone_station_router.login.side_effect = side_effect - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] is not None - assert result["errors"]["base"] == error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) - # Should be recoverable after hits error - with ( - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.get_devices_data", - return_value={ - "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", - "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", - }, - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), - patch( - "homeassistant.components.vodafone_station.async_setup_entry", - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "fake_host", - CONF_USERNAME: "fake_username", - CONF_PASSWORD: "fake_password", - }, - ) - await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": error} - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "fake_host" - assert result2["data"] == { - "host": "fake_host", - "username": "fake_username", - "password": "fake_password", - } + mock_vodafone_station_router.login.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "fake_host" + assert result["data"] == { + "host": "fake_host", + "username": "fake_username", + "password": "fake_password", + } -async def test_reauth_successful(hass: HomeAssistant) -> None: +async def test_duplicate_entry( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test starting a flow by user with a duplicate entry.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "fake_host", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth_successful( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test starting a reauthentication flow.""" - - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - mock_config.add_to_hass(hass) - result = await mock_config.start_reauth_flow(hass) + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with ( - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), - patch( - "homeassistant.components.vodafone_station.async_setup_entry", - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_PASSWORD: "other_fake_password", - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "other_fake_password", + }, + ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" @pytest.mark.parametrize( ("side_effect", "error"), [ - (aiovodafone_exceptions.CannotConnect, "cannot_connect"), - (aiovodafone_exceptions.CannotAuthenticate, "invalid_auth"), - (aiovodafone_exceptions.AlreadyLogged, "already_logged"), + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (AlreadyLogged, "already_logged"), (ConnectionResetError, "unknown"), ], ) -async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> None: +async def test_reauth_not_successful( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: """Test starting a reauthentication flow but no connection found.""" - - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - mock_config.add_to_hass(hass) - - result = await mock_config.start_reauth_flow(hass) + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with ( - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - side_effect=side_effect, - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), - patch( - "homeassistant.components.vodafone_station.async_setup_entry", - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_PASSWORD: "other_fake_password", - }, - ) + mock_vodafone_station_router.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "other_fake_password", + }, + ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] is not None - assert result["errors"]["base"] == error + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": error} - # Should be recoverable after hits error - with ( - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.get_devices_data", - return_value={ - "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", - "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", - }, - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), - patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), - patch( - "homeassistant.components.vodafone_station.async_setup_entry", - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_PASSWORD: "fake_password", - }, - ) - await hass.async_block_till_done() + mock_vodafone_station_router.login.side_effect = None - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "fake_password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_PASSWORD] == "fake_password" -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test options flow.""" - - mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - mock_config.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(mock_config.entry_id) - await hass.async_block_till_done() + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ CONF_CONSIDER_HOME: 37, }, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { diff --git a/tests/components/vodafone_station/test_coordinator.py b/tests/components/vodafone_station/test_coordinator.py new file mode 100644 index 00000000000..1a9470245c7 --- /dev/null +++ b/tests/components/vodafone_station/test_coordinator.py @@ -0,0 +1,68 @@ +"""Define tests for the Vodafone Station coordinator.""" + +import logging +from unittest.mock import AsyncMock + +from aiovodafone import VodafoneStationDevice +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.vodafone_station.const import DOMAIN, SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration +from .const import DEVICE_1_HOST, DEVICE_1_MAC, DEVICE_2_HOST, DEVICE_2_MAC + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_coordinator_device_cleanup( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, + device_registry: dr.DeviceRegistry, +) -> None: + """Test Device cleanup on coordinator update.""" + + caplog.set_level(logging.DEBUG) + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, DEVICE_1_MAC)}, + name=DEVICE_1_HOST, + ) + assert device is not None + + device_tracker = f"device_tracker.{DEVICE_1_HOST}" + + state = hass.states.get(device_tracker) + assert state is not None + + mock_vodafone_station_router.get_devices_data.return_value = { + DEVICE_2_MAC: VodafoneStationDevice( + connected=True, + connection_type="lan", + ip_address="192.168.1.11", + name=DEVICE_2_HOST, + mac=DEVICE_2_MAC, + type="desktop", + wifi="", + ), + } + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(device_tracker) + assert state is None + assert f"Skipping entity {DEVICE_2_HOST}" in caplog.text + + device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_1_MAC)}) + assert device is None + assert f"Removing device: {DEVICE_1_HOST}" in caplog.text diff --git a/tests/components/vodafone_station/test_device_tracker.py b/tests/components/vodafone_station/test_device_tracker.py new file mode 100644 index 00000000000..e172fa76de5 --- /dev/null +++ b/tests/components/vodafone_station/test_device_tracker.py @@ -0,0 +1,64 @@ +"""Define tests for the Vodafone Station device tracker.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.vodafone_station.const import SCAN_INTERVAL +from homeassistant.components.vodafone_station.coordinator import CONSIDER_HOME_SECONDS +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import DEVICE_1_HOST, DEVICE_1_MAC + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.vodafone_station.PLATFORMS", [Platform.DEVICE_TRACKER] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_consider_home( + hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test if device is considered not_home when disconnected.""" + await setup_integration(hass, mock_config_entry) + + device_tracker = f"device_tracker.{DEVICE_1_HOST}" + + state = hass.states.get(device_tracker) + assert state + assert state.state == STATE_HOME + + mock_vodafone_station_router.get_devices_data.return_value[ + DEVICE_1_MAC + ].connected = False + + freezer.tick(SCAN_INTERVAL + CONSIDER_HOME_SECONDS) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(device_tracker) + assert state + assert state.state == STATE_NOT_HOME diff --git a/tests/components/vodafone_station/test_diagnostics.py b/tests/components/vodafone_station/test_diagnostics.py index 02918d81912..5a4a46ce693 100644 --- a/tests/components/vodafone_station/test_diagnostics.py +++ b/tests/components/vodafone_station/test_diagnostics.py @@ -2,16 +2,14 @@ from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock from syrupy import SnapshotAssertion from syrupy.filters import props -from homeassistant.components.vodafone_station.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from .const import DEVICE_DATA_QUERY, MOCK_USER_DATA, SENSOR_DATA_QUERY +from . import setup_integration from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -20,29 +18,17 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) - entry.add_to_hass(hass) + await setup_integration(hass, mock_config_entry) - with ( - patch("aiovodafone.api.VodafoneStationSercommApi.login"), - patch( - "aiovodafone.api.VodafoneStationSercommApi.get_devices_data", - return_value=DEVICE_DATA_QUERY, - ), - patch( - "aiovodafone.api.VodafoneStationSercommApi.get_sensor_data", - return_value=SENSOR_DATA_QUERY, - ), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state == ConfigEntryState.LOADED - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot( exclude=props( "entry_id", "created_at", diff --git a/tests/components/vodafone_station/test_sensor.py b/tests/components/vodafone_station/test_sensor.py new file mode 100644 index 00000000000..ddf97824c75 --- /dev/null +++ b/tests/components/vodafone_station/test_sensor.py @@ -0,0 +1,129 @@ +"""Tests for Vodafone Station sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from aiovodafone import CannotAuthenticate +from aiovodafone.exceptions import AlreadyLogged, CannotConnect +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.vodafone_station.const import LINE_TYPES, SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.vodafone_station.PLATFORMS", [Platform.SENSOR] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("connection_type", "index"), + [ + ("dsl_ipaddr", 0), + ("fiber_ipaddr", 1), + ("vf_internet_key_ip_addr", 2), + ], +) +async def test_active_connection_type( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + connection_type: str, + index: int, +) -> None: + """Test device connection type.""" + await setup_integration(hass, mock_config_entry) + + active_connection_entity = "sensor.vodafone_station_m123456789_active_connection" + + state = hass.states.get(active_connection_entity) + assert state + assert state.state == STATE_UNKNOWN + + mock_vodafone_station_router.get_sensor_data.return_value[connection_type] = ( + "1.1.1.1" + ) + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(active_connection_entity) + assert state + assert state.state == LINE_TYPES[index] + + +@pytest.mark.freeze_time("2023-12-02T13:00:00+00:00") +async def test_uptime( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test device uptime shift.""" + await setup_integration(hass, mock_config_entry) + + uptime = "2024-11-19T20:19:00+00:00" + uptime_entity = "sensor.vodafone_station_m123456789_uptime" + + state = hass.states.get(uptime_entity) + assert state + assert state.state == uptime + + mock_vodafone_station_router.get_sensor_data.return_value["sys_uptime"] = "12:17:23" + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(uptime_entity) + assert state + assert state.state == uptime + + +@pytest.mark.parametrize( + "side_effect", + [ + CannotConnect, + CannotAuthenticate, + AlreadyLogged, + ConnectionResetError, + ], +) +async def test_coordinator_client_connector_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_vodafone_station_router: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, +) -> None: + """Test ClientConnectorError on coordinator update.""" + await setup_integration(hass, mock_config_entry) + + mock_vodafone_station_router.get_devices_data.side_effect = side_effect + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("sensor.vodafone_station_m123456789_uptime") + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/voip/conftest.py b/tests/components/voip/conftest.py index 99707297230..d47db58d585 100644 --- a/tests/components/voip/conftest.py +++ b/tests/components/voip/conftest.py @@ -57,6 +57,7 @@ def call_info() -> CallInfo: """Fake call info.""" return CallInfo( caller_endpoint=get_sip_endpoint("192.168.1.210", 5060), + local_endpoint=get_sip_endpoint("192.168.1.10", 5060), caller_rtp_port=5004, server_ip="192.168.1.10", headers={ diff --git a/tests/components/voip/test_config_flow.py b/tests/components/voip/test_config_flow.py index 1b7aaad7c03..05f14afa4e7 100644 --- a/tests/components/voip/test_config_flow.py +++ b/tests/components/voip/test_config_flow.py @@ -80,3 +80,30 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options == {"sip_port": 5061} + + # Manual with user + result = await hass.config_entries.options.async_init( + config_entry.entry_id, + ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"sip_port": 5061, "sip_user": "HA"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == {"sip_port": 5061, "sip_user": "HA"} + + # Manual remove user + result = await hass.config_entries.options.async_init( + config_entry.entry_id, + ) + + assert config_entry.options == {"sip_port": 5061, "sip_user": "HA"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"sip_port": 5060, "sip_user": ""}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.options == {"sip_port": 5060} diff --git a/tests/components/voip/test_devices.py b/tests/components/voip/test_devices.py index 55359b8407d..4e2e129d4be 100644 --- a/tests/components/voip/test_devices.py +++ b/tests/components/voip/test_devices.py @@ -2,12 +2,15 @@ from __future__ import annotations +import pytest from voip_utils import CallInfo from homeassistant.components.voip import DOMAIN from homeassistant.components.voip.devices import VoIPDevice, VoIPDevices from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry async def test_device_registry_info( @@ -21,10 +24,10 @@ async def test_device_registry_info( assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device( - identifiers={(DOMAIN, call_info.caller_ip)} + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} ) assert device is not None - assert device.name == call_info.caller_ip + assert device.name == call_info.caller_endpoint.host assert device.manufacturer == "Grandstream" assert device.model == "HT801" assert device.sw_version == "1.0.17.5" @@ -36,7 +39,7 @@ async def test_device_registry_info( assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device( - identifiers={(DOMAIN, call_info.caller_ip)} + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} ) assert device.sw_version == "2.0.0.0" @@ -53,7 +56,7 @@ async def test_device_registry_info_from_unknown_phone( assert not voip_device.async_allow_call(hass) device = device_registry.async_get_device( - identifiers={(DOMAIN, call_info.caller_ip)} + identifiers={(DOMAIN, call_info.caller_endpoint.uri)} ) assert device.manufacturer is None assert device.model == "Unknown" @@ -76,3 +79,53 @@ async def test_remove_device_registry_entry( assert hass.states.get("switch.192_168_1_210_allow_calls") is None assert voip_device.voip_id not in voip_devices.devices + + +@pytest.fixture +async def legacy_dev_reg_entry( + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, + call_info: CallInfo, +) -> None: + """Fixture to run before we set up the VoIP integration via fixture.""" + device = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, call_info.caller_ip)}, + ) + entity_registry.async_get_or_create( + "switch", + DOMAIN, + f"{call_info.caller_ip}-allow_calls", + device_id=device.id, + config_entry=config_entry, + ) + return device + + +async def test_device_registry_migration( + hass: HomeAssistant, + legacy_dev_reg_entry: dr.DeviceEntry, + voip_devices: VoIPDevices, + call_info: CallInfo, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test info in device registry migrates old devices.""" + voip_device = voip_devices.async_get_or_create(call_info) + new_id = call_info.caller_endpoint.uri + assert voip_device.voip_id == new_id + + device = device_registry.async_get_device(identifiers={(DOMAIN, new_id)}) + assert device is not None + assert device.id == legacy_dev_reg_entry.id + assert device.identifiers == {(DOMAIN, new_id)} + assert device.name == call_info.caller_endpoint.host + assert device.manufacturer == "Grandstream" + assert device.model == "HT801" + assert device.sw_version == "1.0.17.5" + + assert ( + entity_registry.async_get_entity_id("switch", DOMAIN, f"{new_id}-allow_calls") + is not None + ) diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 17af2748c1c..3e3e5337417 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -16,7 +16,7 @@ from homeassistant.components.assist_satellite import AssistSatelliteEntity # pylint: disable-next=hass-component-root-import from homeassistant.components.assist_satellite.entity import AssistSatelliteState -from homeassistant.components.voip import HassVoipDatagramProtocol +from homeassistant.components.voip import DOMAIN, HassVoipDatagramProtocol from homeassistant.components.voip.assist_satellite import Tones, VoipAssistSatellite from homeassistant.components.voip.devices import VoIPDevice, VoIPDevices from homeassistant.components.voip.voip import PreRecordMessageProtocol, make_protocol @@ -844,3 +844,330 @@ async def test_pipeline_error( assert sum(played_audio_bytes) > 0 assert played_audio_bytes == snapshot() + + +@pytest.mark.usefixtures("socket_enabled") +async def test_announce( + hass: HomeAssistant, + voip_devices: VoIPDevices, + voip_device: VoIPDevice, +) -> None: + """Test announcement.""" + assert await async_setup_component(hass, "voip", {}) + + satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + assert isinstance(satellite, VoipAssistSatellite) + assert ( + satellite.supported_features + & assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE + ) + + announcement = assist_satellite.AssistSatelliteAnnouncement( + message="test announcement", + media_id=_MEDIA_ID, + original_media_id=_MEDIA_ID, + media_id_source="tts", + ) + + # Protocol has already been mocked, but "outgoing_call" is not async + mock_protocol: AsyncMock = hass.data[DOMAIN].protocol + mock_protocol.outgoing_call = Mock() + + with ( + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", + ) as mock_send_tts, + ): + satellite.transport = Mock() + announce_task = hass.async_create_background_task( + satellite.async_announce(announcement), "voip_announce" + ) + await asyncio.sleep(0) + mock_protocol.outgoing_call.assert_called_once() + + # Trigger announcement + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(1): + await announce_task + + mock_send_tts.assert_called_once_with(_MEDIA_ID, wait_for_tone=False) + + +@pytest.mark.usefixtures("socket_enabled") +async def test_voip_id_is_ip_address( + hass: HomeAssistant, + voip_devices: VoIPDevices, + voip_device: VoIPDevice, +) -> None: + """Test announcement when VoIP is an IP address instead of a SIP header.""" + assert await async_setup_component(hass, "voip", {}) + + satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + assert isinstance(satellite, VoipAssistSatellite) + assert ( + satellite.supported_features + & assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE + ) + + announcement = assist_satellite.AssistSatelliteAnnouncement( + message="test announcement", + media_id=_MEDIA_ID, + original_media_id=_MEDIA_ID, + media_id_source="tts", + ) + + # Protocol has already been mocked, but "outgoing_call" is not async + mock_protocol: AsyncMock = hass.data[DOMAIN].protocol + mock_protocol.outgoing_call = Mock() + + with ( + patch.object(voip_device, "voip_id", "192.168.68.10"), + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", + ) as mock_send_tts, + ): + satellite.transport = Mock() + announce_task = hass.async_create_background_task( + satellite.async_announce(announcement), "voip_announce" + ) + await asyncio.sleep(0) + mock_protocol.outgoing_call.assert_called_once() + assert ( + mock_protocol.outgoing_call.call_args.kwargs["destination"].host + == "192.168.68.10" + ) + + # Trigger announcement + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(1): + await announce_task + + mock_send_tts.assert_called_once_with(_MEDIA_ID, wait_for_tone=False) + + +@pytest.mark.usefixtures("socket_enabled") +async def test_announce_timeout( + hass: HomeAssistant, + voip_devices: VoIPDevices, + voip_device: VoIPDevice, +) -> None: + """Test announcement when user does not pick up the phone in time.""" + assert await async_setup_component(hass, "voip", {}) + + satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + assert isinstance(satellite, VoipAssistSatellite) + assert ( + satellite.supported_features + & assist_satellite.AssistSatelliteEntityFeature.ANNOUNCE + ) + + announcement = assist_satellite.AssistSatelliteAnnouncement( + message="test announcement", + media_id=_MEDIA_ID, + original_media_id=_MEDIA_ID, + media_id_source="tts", + ) + + # Protocol has already been mocked, but some methods are not async + mock_protocol: AsyncMock = hass.data[DOMAIN].protocol + mock_protocol.outgoing_call = Mock() + mock_protocol.cancel_call = Mock() + + # Very short timeout which will trigger because we don't send any audio in + with ( + patch( + "homeassistant.components.voip.assist_satellite._ANNOUNCEMENT_RING_TIMEOUT", + 0.01, + ), + ): + satellite.transport = Mock() + with pytest.raises(TimeoutError): + await satellite.async_announce(announcement) + + +@pytest.mark.usefixtures("socket_enabled") +async def test_start_conversation( + hass: HomeAssistant, + voip_devices: VoIPDevices, + voip_device: VoIPDevice, +) -> None: + """Test start conversation.""" + assert await async_setup_component(hass, "voip", {}) + + satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + assert isinstance(satellite, VoipAssistSatellite) + assert ( + satellite.supported_features + & assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION + ) + + announcement = assist_satellite.AssistSatelliteAnnouncement( + message="test announcement", + media_id=_MEDIA_ID, + original_media_id=_MEDIA_ID, + media_id_source="tts", + ) + + # Protocol has already been mocked, but "outgoing_call" is not async + mock_protocol: AsyncMock = hass.data[DOMAIN].protocol + mock_protocol.outgoing_call = Mock() + + tts_sent = asyncio.Event() + + async def _send_tts(*args, **kwargs): + tts_sent.set() + + async def async_pipeline_from_audio_stream( + hass: HomeAssistant, + context: Context, + *args, + device_id: str | None, + tts_audio_output: str | dict[str, Any] | None, + **kwargs, + ): + event_callback = kwargs["event_callback"] + + # Fake tts result + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.TTS_START, + data={ + "engine": "test", + "language": hass.config.language, + "voice": "test", + "tts_input": "fake-text", + }, + ) + ) + + # Proceed with media output + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.TTS_END, + data={"tts_output": {"media_id": _MEDIA_ID}}, + ) + ) + + event_callback( + assist_pipeline.PipelineEvent( + type=assist_pipeline.PipelineEventType.RUN_END + ) + ) + + with ( + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", + new=_send_tts, + ), + patch( + "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + ): + satellite.transport = Mock() + conversation_task = hass.async_create_background_task( + satellite.async_start_conversation(announcement), "voip_start_conversation" + ) + await asyncio.sleep(0) + mock_protocol.outgoing_call.assert_called_once() + + # Trigger announcement and wait for it to finish + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(1): + await tts_sent.wait() + + tts_sent.clear() + + # Trigger pipeline + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(1): + # Wait for TTS + await tts_sent.wait() + await conversation_task + + +@pytest.mark.usefixtures("socket_enabled") +async def test_start_conversation_user_doesnt_pick_up( + hass: HomeAssistant, + voip_devices: VoIPDevices, + voip_device: VoIPDevice, +) -> None: + """Test start conversation when the user doesn't pick up.""" + assert await async_setup_component(hass, "voip", {}) + + pipeline = assist_pipeline.Pipeline( + conversation_engine="test engine", + conversation_language="en", + language="en", + name="test pipeline", + stt_engine="test stt", + stt_language="en", + tts_engine="test tts", + tts_language="en", + tts_voice=None, + wake_word_entity=None, + wake_word_id=None, + ) + + satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + assert isinstance(satellite, VoipAssistSatellite) + assert ( + satellite.supported_features + & assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION + ) + + # Protocol has already been mocked, but "outgoing_call" is not async + mock_protocol: AsyncMock = hass.data[DOMAIN].protocol + mock_protocol.outgoing_call = Mock() + + pipeline_started = asyncio.Event() + + async def async_pipeline_from_audio_stream( + hass: HomeAssistant, + context: Context, + *args, + conversation_extra_system_prompt: str | None = None, + **kwargs, + ): + # System prompt should be not be set due to timeout (user not picking up) + assert conversation_extra_system_prompt is None + + pipeline_started.set() + + with ( + patch( + "homeassistant.components.assist_satellite.entity.async_get_pipeline", + return_value=pipeline, + ), + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite.async_start_conversation", + side_effect=TimeoutError, + ), + patch( + "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.assist_satellite.entity.tts_generate_media_source_id", + return_value="test media id", + ), + ): + satellite.transport = Mock() + + # Error should clear system prompt + with pytest.raises(TimeoutError): + await hass.services.async_call( + assist_satellite.DOMAIN, + "start_conversation", + { + "entity_id": satellite.entity_id, + "start_message": "test announcement", + "extra_system_prompt": "test prompt", + }, + blocking=True, + ) + + # Trigger a pipeline so we can check if the system prompt was cleared + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(1): + await pipeline_started.wait() diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 9c3708f970c..85e9e250ab9 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -4,11 +4,11 @@ from ipaddress import ip_address from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.volumio.config_flow import CannotConnectError from homeassistant.components.volumio.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -21,7 +21,7 @@ TEST_CONNECTION = { } -TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( +TEST_DISCOVERY = ZeroconfServiceInfo( ip_address=ip_address("1.1.1.1"), ip_addresses=[ip_address("1.1.1.1")], hostname="mock_hostname", diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index f9f922b35d4..65be23fc168 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -4,8 +4,7 @@ import pytest import voluptuous as vol from homeassistant.components import vultr as base_vultr -from homeassistant.components.vultr import CONF_SUBSCRIPTION -import homeassistant.components.vultr.sensor as vultr +from homeassistant.components.vultr import CONF_SUBSCRIPTION, sensor as vultr from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index cdaf7e0e3f0..e6e8ff72a6d 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components import wake_word from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, State -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.setup import async_setup_component from .common import mock_wake_word_entity_platform @@ -143,7 +143,7 @@ async def mock_config_entry_setup( async def async_setup_entry_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test stt platform via config entry.""" async_add_entities([mock_provider_entity]) diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index 78efd94ef8e..191acdf24f9 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -2,17 +2,20 @@ from __future__ import annotations +from typing import Any from unittest import mock from unittest.mock import AsyncMock, MagicMock import pytest import voluptuous as vol +from homeassistant.components import water_heater from homeassistant.components.water_heater import ( DOMAIN, SERVICE_SET_OPERATION_MODE, SET_TEMPERATURE_SCHEMA, WaterHeaterEntity, + WaterHeaterEntityDescription, WaterHeaterEntityFeature, ) from homeassistant.config_entries import ConfigEntry @@ -20,13 +23,14 @@ from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import ( MockConfigEntry, MockModule, MockPlatform, async_mock_service, + import_and_test_deprecated_constant, mock_integration, mock_platform, ) @@ -141,7 +145,7 @@ async def test_operation_mode_validation( async def async_setup_entry_water_heater_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test water_heater platform via config entry.""" async_add_entities([water_heater_entity]) @@ -204,3 +208,30 @@ async def test_operation_mode_validation( ) await hass.async_block_till_done() water_heater_entity.set_operation_mode.assert_has_calls([mock.call("eco")]) + + +@pytest.mark.parametrize( + ("constant_name", "replacement_name", "replacement"), + [ + ( + "WaterHeaterEntityEntityDescription", + "WaterHeaterEntityDescription", + WaterHeaterEntityDescription, + ), + ], +) +def test_deprecated_constants( + caplog: pytest.LogCaptureFixture, + constant_name: str, + replacement_name: str, + replacement: Any, +) -> None: + """Test deprecated automation constants.""" + import_and_test_deprecated_constant( + caplog, + water_heater, + constant_name, + replacement_name, + replacement, + "2026.1", + ) diff --git a/tests/components/watergate/conftest.py b/tests/components/watergate/conftest.py index d29b90431a4..6d40a4b7152 100644 --- a/tests/components/watergate/conftest.py +++ b/tests/components/watergate/conftest.py @@ -9,7 +9,9 @@ from homeassistant.const import CONF_IP_ADDRESS from .const import ( DEFAULT_DEVICE_STATE, + DEFAULT_NETWORKING_STATE, DEFAULT_SERIAL_NUMBER, + DEFAULT_TELEMETRY_STATE, MOCK_CONFIG, MOCK_WEBHOOK_ID, ) @@ -35,6 +37,12 @@ def mock_watergate_client() -> Generator[AsyncMock]: mock_client_instance.async_get_device_state = AsyncMock( return_value=DEFAULT_DEVICE_STATE ) + mock_client_instance.async_get_networking = AsyncMock( + return_value=DEFAULT_NETWORKING_STATE + ) + mock_client_instance.async_get_telemetry_data = AsyncMock( + return_value=DEFAULT_TELEMETRY_STATE + ) yield mock_client_instance diff --git a/tests/components/watergate/const.py b/tests/components/watergate/const.py index 4297b3321ad..0f7cc12c14b 100644 --- a/tests/components/watergate/const.py +++ b/tests/components/watergate/const.py @@ -1,6 +1,7 @@ """Constants for the Watergate tests.""" -from watergate_local_api.models import DeviceState +from watergate_local_api.models import DeviceState, NetworkingData, TelemetryData +from watergate_local_api.models.water_meter import WaterMeter from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, CONF_WEBHOOK_ID @@ -22,6 +23,20 @@ DEFAULT_DEVICE_STATE = DeviceState( "battery", "1.0.0", 100, - {"volume": 1.2, "duration": 100}, + WaterMeter(1.2, 100), DEFAULT_SERIAL_NUMBER, ) + +DEFAULT_NETWORKING_STATE = NetworkingData( + True, + True, + "192.168.1.127", + "192.168.1.1", + "255.255.255.0", + "Sonic", + -50, + 2137, + 1910, +) + +DEFAULT_TELEMETRY_STATE = TelemetryData(0.0, 100, 28.32, None, []) diff --git a/tests/components/watergate/snapshots/test_sensor.ambr b/tests/components/watergate/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..b4b6c4ee0a4 --- /dev/null +++ b/tests/components/watergate/snapshots/test_sensor.ambr @@ -0,0 +1,516 @@ +# serializer version: 1 +# name: test_sensor[sensor.sonic_mqtt_up_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_mqtt_up_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'MQTT up since', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mqtt_up_since', + 'unique_id': 'a63182948ce2896a.mqtt_up_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_mqtt_up_since-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sonic MQTT up since', + }), + 'context': , + 'entity_id': 'sensor.sonic_mqtt_up_since', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-09T11:59:58+00:00', + }) +# --- +# name: test_sensor[sensor.sonic_power_supply_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'battery', + 'external', + 'battery_external', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_power_supply_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power supply mode', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_supply_mode', + 'unique_id': 'a63182948ce2896a.power_supply_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_power_supply_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Sonic Power supply mode', + 'options': list([ + 'battery', + 'external', + 'battery_external', + ]), + }), + 'context': , + 'entity_id': 'sensor.sonic_power_supply_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'battery', + }) +# --- +# name: test_sensor[sensor.sonic_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a63182948ce2896a.rssi', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensor[sensor.sonic_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Sonic Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.sonic_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-50', + }) +# --- +# name: test_sensor[sensor.sonic_up_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_up_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Up since', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'up_since', + 'unique_id': 'a63182948ce2896a.up_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_up_since-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sonic Up since', + }), + 'context': , + 'entity_id': 'sensor.sonic_up_since', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-09T11:58:20+00:00', + }) +# --- +# name: test_sensor[sensor.sonic_volume_flow_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_volume_flow_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volume flow rate', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a63182948ce2896a.water_flow_rate', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_volume_flow_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Sonic Volume flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_volume_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_meter_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water meter duration', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_meter_duration', + 'unique_id': 'a63182948ce2896a.water_meter_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Sonic Water meter duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_meter_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_meter_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water meter volume', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_meter_volume', + 'unique_id': 'a63182948ce2896a.water_meter_volume', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Sonic Water meter volume', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_meter_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.2', + }) +# --- +# name: test_sensor[sensor.sonic_water_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water pressure', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_pressure', + 'unique_id': 'a63182948ce2896a.water_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Sonic Water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.sonic_water_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water temperature', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_temperature', + 'unique_id': 'a63182948ce2896a.water_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Sonic Water temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.32', + }) +# --- +# name: test_sensor[sensor.sonic_wi_fi_up_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_wi_fi_up_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wi-Fi up since', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_up_since', + 'unique_id': 'a63182948ce2896a.wifi_up_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_wi_fi_up_since-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sonic Wi-Fi up since', + }), + 'context': , + 'entity_id': 'sensor.sonic_wi_fi_up_since', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-09T11:59:57+00:00', + }) +# --- diff --git a/tests/components/watergate/test_sensor.py b/tests/components/watergate/test_sensor.py new file mode 100644 index 00000000000..78e375857ed --- /dev/null +++ b/tests/components/watergate/test_sensor.py @@ -0,0 +1,150 @@ +"""Tests for the Watergate valve platform.""" + +from collections.abc import Generator + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import EntityCategory, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration +from .const import DEFAULT_NETWORKING_STATE, DEFAULT_TELEMETRY_STATE, MOCK_WEBHOOK_ID + +from tests.common import AsyncMock, MockConfigEntry, patch, snapshot_platform +from tests.typing import ClientSessionGenerator + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test states of the sensor.""" + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch("homeassistant.components.watergate.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, mock_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) + + +async def test_diagnostics_are_disabled_by_default( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if all diagnostic entities are disabled by default.""" + with patch("homeassistant.components.watergate.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, mock_entry) + + entries = [ + entry + for entry in entity_registry.entities.get_entries_for_config_entry_id( + mock_entry.entry_id + ) + if entry.entity_category == EntityCategory.DIAGNOSTIC + ] + + assert len(entries) == 5 + for entry in entries: + assert entry.disabled + + +async def test_telemetry_webhook( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if water flow webhook is handled correctly.""" + await init_integration(hass, mock_entry) + + def assert_state(entity_id: str, expected_state: str): + state = hass.states.get(entity_id) + assert state.state == str(expected_state) + + assert_state("sensor.sonic_volume_flow_rate", DEFAULT_TELEMETRY_STATE.flow) + assert_state("sensor.sonic_water_pressure", DEFAULT_TELEMETRY_STATE.pressure) + assert_state( + "sensor.sonic_water_temperature", DEFAULT_TELEMETRY_STATE.water_temperature + ) + + telemetry_change_data = { + "type": "telemetry", + "data": {"flow": 2137, "pressure": 1910, "temperature": 20}, + } + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=telemetry_change_data) + + await hass.async_block_till_done() + + assert_state("sensor.sonic_volume_flow_rate", "2.137") + assert_state("sensor.sonic_water_pressure", "1910") + assert_state("sensor.sonic_water_temperature", "20") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_wifi_webhook( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if water flow webhook is handled correctly.""" + await init_integration(hass, mock_entry) + + def assert_state(entity_id: str, expected_state: str): + state = hass.states.get(entity_id) + assert state.state == str(expected_state) + + assert_state("sensor.sonic_signal_strength", DEFAULT_NETWORKING_STATE.rssi) + + wifi_change_data = { + "type": "wifi-changed", + "data": { + "ip": "192.168.2.137", + "gateway": "192.168.2.1", + "ssid": "Sonic 2", + "rssi": -70, + "subnet": "255.255.255.0", + }, + } + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=wifi_change_data) + + await hass.async_block_till_done() + + assert_state("sensor.sonic_signal_strength", "-70") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_power_supply_webhook( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if water flow webhook is handled correctly.""" + await init_integration(hass, mock_entry) + entity_id = "sensor.sonic_power_supply_mode" + registered_entity = hass.states.get(entity_id) + assert registered_entity + assert registered_entity.state == "battery" + + power_supply_change_data = { + "type": "power-supply-changed", + "data": {"supply": "external_battery"}, + } + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=power_supply_change_data) + + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "battery_external" diff --git a/tests/components/weather/__init__.py b/tests/components/weather/__init__.py index 2dbffbbd617..301e055129d 100644 --- a/tests/components/weather/__init__.py +++ b/tests/components/weather/__init__.py @@ -21,7 +21,7 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import ( MockConfigEntry, @@ -90,7 +90,7 @@ async def create_entity( async def async_setup_entry_weather_platform( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up test weather platform via config entry.""" async_add_entities([weather_entity]) diff --git a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr index 95be86664a2..c06229302c5 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -62,6 +63,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -117,6 +119,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -172,6 +175,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -227,6 +231,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -277,6 +282,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -327,6 +333,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -377,6 +384,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -427,6 +435,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -477,6 +486,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -535,6 +545,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -593,6 +604,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -648,6 +660,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -703,6 +716,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -758,6 +772,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/weatherflow_cloud/snapshots/test_weather.ambr b/tests/components/weatherflow_cloud/snapshots/test_weather.ambr index 569b744529c..0b0d66c34a7 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_weather.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_weather.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/weatherkit/__init__.py b/tests/components/weatherkit/__init__.py index 99c856a7e37..bc7c31fe8d8 100644 --- a/tests/components/weatherkit/__init__.py +++ b/tests/components/weatherkit/__init__.py @@ -1,5 +1,6 @@ """Tests for the Apple WeatherKit integration.""" +from contextlib import contextmanager from unittest.mock import patch from apple_weatherkit import DataSetType @@ -26,20 +27,13 @@ EXAMPLE_CONFIG_DATA = { } -async def init_integration( - hass: HomeAssistant, +@contextmanager +def mock_weather_response( is_night_time: bool = False, has_hourly_forecast: bool = True, has_daily_forecast: bool = True, -) -> MockConfigEntry: - """Set up the WeatherKit integration in Home Assistant.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Home", - unique_id="0123456", - data=EXAMPLE_CONFIG_DATA, - ) - +): + """Mock a successful WeatherKit API response.""" weather_response = load_json_object_fixture("weatherkit/weather_response.json") available_data_sets = [DataSetType.CURRENT_WEATHER] @@ -68,8 +62,22 @@ async def init_integration( return_value=available_data_sets, ), ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + yield + + +async def init_integration( + hass: HomeAssistant, +) -> MockConfigEntry: + """Set up the WeatherKit integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="0123456", + data=EXAMPLE_CONFIG_DATA, + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/weatherkit/test_coordinator.py b/tests/components/weatherkit/test_coordinator.py index eff142f3d94..7cc78179f44 100644 --- a/tests/components/weatherkit/test_coordinator.py +++ b/tests/components/weatherkit/test_coordinator.py @@ -4,30 +4,93 @@ from datetime import timedelta from unittest.mock import patch from apple_weatherkit.client import WeatherKitApiClientError +from freezegun.api import FrozenDateTimeFactory from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow -from . import init_integration +from . import init_integration, mock_weather_response from tests.common import async_fire_time_changed -async def test_failed_updates(hass: HomeAssistant) -> None: - """Test that we properly handle failed updates.""" - await init_integration(hass) +async def test_update_uses_stale_data_before_threshold( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that stale data from the last successful update is used if an update failure occurs before the threshold.""" + with mock_weather_response(): + await init_integration(hass) + + state = hass.states.get("weather.home") + assert state + assert state.state != STATE_UNAVAILABLE + + initial_state = state.state + + # Expect stale data to be used before one hour with patch( "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", side_effect=WeatherKitApiClientError, ): - async_fire_time_changed( - hass, - utcnow() + timedelta(minutes=5), - ) + freezer.tick(timedelta(minutes=59)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("weather.home") + assert state + assert state.state == initial_state + + +async def test_update_becomes_unavailable_after_threshold( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that the entity becomes unavailable if an update failure occurs after the threshold.""" + with mock_weather_response(): + await init_integration(hass) + + # Expect state to be unavailable after one hour + + with patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", + side_effect=WeatherKitApiClientError, + ): + freezer.tick(timedelta(hours=1, minutes=5)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("weather.home") assert state assert state.state == STATE_UNAVAILABLE + + +async def test_update_recovers_after_failure( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that a successful update after repeated failures recovers the entity's state.""" + with mock_weather_response(): + await init_integration(hass) + + # Trigger a failure after threshold + + with patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", + side_effect=WeatherKitApiClientError, + ): + freezer.tick(timedelta(hours=1, minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Expect that a successful update recovers the entity + + with mock_weather_response(): + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("weather.home") + assert state + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/weatherkit/test_sensor.py b/tests/components/weatherkit/test_sensor.py index 6c6999c6bfd..6ded9a779d5 100644 --- a/tests/components/weatherkit/test_sensor.py +++ b/tests/components/weatherkit/test_sensor.py @@ -6,7 +6,7 @@ import pytest from homeassistant.core import HomeAssistant -from . import init_integration +from . import init_integration, mock_weather_response @pytest.mark.parametrize( @@ -20,7 +20,8 @@ async def test_sensor_values( hass: HomeAssistant, entity_name: str, expected_value: Any ) -> None: """Test that various sensor values match what we expect.""" - await init_integration(hass) + with mock_weather_response(): + await init_integration(hass) state = hass.states.get(entity_name) assert state diff --git a/tests/components/weatherkit/test_weather.py b/tests/components/weatherkit/test_weather.py index ba20276c22e..ec4ce2ba3b3 100644 --- a/tests/components/weatherkit/test_weather.py +++ b/tests/components/weatherkit/test_weather.py @@ -23,12 +23,13 @@ from homeassistant.components.weatherkit.const import ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant -from . import init_integration +from . import init_integration, mock_weather_response async def test_current_weather(hass: HomeAssistant) -> None: """Test states of the current weather.""" - await init_integration(hass) + with mock_weather_response(): + await init_integration(hass) state = hass.states.get("weather.home") assert state @@ -49,7 +50,8 @@ async def test_current_weather(hass: HomeAssistant) -> None: async def test_current_weather_nighttime(hass: HomeAssistant) -> None: """Test that the condition is clear-night when it's sunny and night time.""" - await init_integration(hass, is_night_time=True) + with mock_weather_response(is_night_time=True): + await init_integration(hass) state = hass.states.get("weather.home") assert state @@ -58,7 +60,8 @@ async def test_current_weather_nighttime(hass: HomeAssistant) -> None: async def test_daily_forecast_missing(hass: HomeAssistant) -> None: """Test that daily forecast is not supported when WeatherKit doesn't support it.""" - await init_integration(hass, has_daily_forecast=False) + with mock_weather_response(has_daily_forecast=False): + await init_integration(hass) state = hass.states.get("weather.home") assert state @@ -69,7 +72,8 @@ async def test_daily_forecast_missing(hass: HomeAssistant) -> None: async def test_hourly_forecast_missing(hass: HomeAssistant) -> None: """Test that hourly forecast is not supported when WeatherKit doesn't support it.""" - await init_integration(hass, has_hourly_forecast=False) + with mock_weather_response(has_hourly_forecast=False): + await init_integration(hass) state = hass.states.get("weather.home") assert state @@ -86,7 +90,8 @@ async def test_hourly_forecast( hass: HomeAssistant, snapshot: SnapshotAssertion, service: str ) -> None: """Test states of the hourly forecast.""" - await init_integration(hass) + with mock_weather_response(): + await init_integration(hass) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -109,7 +114,8 @@ async def test_daily_forecast( hass: HomeAssistant, snapshot: SnapshotAssertion, service: str ) -> None: """Test states of the daily forecast.""" - await init_integration(hass) + with mock_weather_response(): + await init_integration(hass) response = await hass.services.async_call( WEATHER_DOMAIN, diff --git a/tests/components/webmin/snapshots/test_sensor.ambr b/tests/components/webmin/snapshots/test_sensor.ambr index 6af768d63a8..a2068f662ba 100644 --- a/tests/components/webmin/snapshots/test_sensor.ambr +++ b/tests/components/webmin/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -57,6 +58,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -106,6 +108,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -155,6 +158,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -212,6 +216,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -269,6 +274,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -326,6 +332,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -376,6 +383,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -426,6 +434,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -476,6 +485,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -525,6 +535,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -574,6 +585,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -623,6 +635,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -680,6 +693,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -737,6 +751,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -794,6 +809,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -844,6 +860,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -894,6 +911,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -944,6 +962,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -993,6 +1012,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1042,6 +1062,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1091,6 +1112,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1148,6 +1170,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1205,6 +1228,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1262,6 +1286,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1319,6 +1344,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1376,6 +1402,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1433,6 +1460,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1482,6 +1510,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1531,6 +1560,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1580,6 +1610,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1637,6 +1668,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1694,6 +1726,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1751,6 +1784,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/webostv/__init__.py b/tests/components/webostv/__init__.py index d6c096f9d3a..d9a0a135023 100644 --- a/tests/components/webostv/__init__.py +++ b/tests/components/webostv/__init__.py @@ -1,9 +1,8 @@ -"""Tests for the WebOS TV integration.""" +"""Tests for the LG webOS TV integration.""" from homeassistant.components.webostv.const import DOMAIN from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import CLIENT_KEY, FAKE_UUID, HOST, TV_NAME @@ -25,11 +24,7 @@ async def setup_webostv( ) entry.add_to_hass(hass) - await async_setup_component( - hass, - DOMAIN, - {DOMAIN: {CONF_HOST: HOST}}, - ) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() return entry diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index a30ae933cca..c6594746cc5 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -1,4 +1,4 @@ -"""Common fixtures and objects for the LG webOS integration tests.""" +"""Common fixtures and objects for the LG webOS TV integration tests.""" from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch @@ -7,7 +7,15 @@ import pytest from homeassistant.components.webostv.const import LIVE_TV_APP_ID -from .const import CHANNEL_1, CHANNEL_2, CLIENT_KEY, FAKE_UUID, MOCK_APPS, MOCK_INPUTS +from .const import ( + CHANNEL_1, + CHANNEL_2, + CLIENT_KEY, + FAKE_UUID, + MOCK_APPS, + MOCK_INPUTS, + TV_MODEL, +) @pytest.fixture @@ -22,13 +30,19 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="client") def client_fixture(): """Patch of client library for tests.""" - with patch( - "homeassistant.components.webostv.WebOsClient", autospec=True - ) as mock_client_class: + with ( + patch( + "homeassistant.components.webostv.WebOsClient", autospec=True + ) as mock_client_class, + patch( + "homeassistant.components.webostv.config_flow.WebOsClient", + new=mock_client_class, + ), + ): client = mock_client_class.return_value client.hello_info = {"deviceUUID": FAKE_UUID} client.software_info = {"major_ver": "major", "minor_ver": "minor"} - client.system_info = {"modelName": "TVFAKE"} + client.system_info = {"modelName": TV_MODEL, "serialNumber": "1234567890"} client.client_key = CLIENT_KEY client.apps = MOCK_APPS client.inputs = MOCK_INPUTS diff --git a/tests/components/webostv/const.py b/tests/components/webostv/const.py index afaed224e83..a63a4fe3289 100644 --- a/tests/components/webostv/const.py +++ b/tests/components/webostv/const.py @@ -1,11 +1,13 @@ -"""Constants for LG webOS Smart TV tests.""" +"""Constants for LG webOS TV tests.""" from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.webostv.const import LIVE_TV_APP_ID +from homeassistant.util import slugify FAKE_UUID = "some-fake-uuid" -TV_NAME = "fake_webos" -ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}" +TV_MODEL = "MODEL" +TV_NAME = f"LG webOS TV {TV_MODEL}" +ENTITY_ID = f"{MP_DOMAIN}.{slugify(TV_NAME)}" HOST = "1.2.3.4" CLIENT_KEY = "some-secret" diff --git a/tests/components/webostv/snapshots/test_diagnostics.ambr b/tests/components/webostv/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..030554b963a --- /dev/null +++ b/tests/components/webostv/snapshots/test_diagnostics.ambr @@ -0,0 +1,69 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'client': dict({ + 'apps': dict({ + 'com.webos.app.livetv': dict({ + 'icon': '**REDACTED**', + 'id': 'com.webos.app.livetv', + 'largeIcon': '**REDACTED**', + 'title': 'Live TV', + }), + }), + 'current_app_id': 'com.webos.app.livetv', + 'current_channel': dict({ + 'channelId': 'ch1id', + 'channelName': 'Channel 1', + 'channelNumber': '1', + }), + 'hello_info': dict({ + 'deviceUUID': '**REDACTED**', + }), + 'inputs': dict({ + 'in1': dict({ + 'appId': 'app0', + 'id': 'in1', + 'label': 'Input01', + }), + 'in2': dict({ + 'appId': 'app1', + 'id': 'in2', + 'label': 'Input02', + }), + }), + 'is_connected': True, + 'is_on': True, + 'is_registered': True, + 'software_info': dict({ + 'major_ver': 'major', + 'minor_ver': 'minor', + }), + 'sound_output': 'speaker', + 'system_info': dict({ + 'modelName': 'MODEL', + 'serialNumber': '1234567890', + }), + }), + 'entry': dict({ + 'data': dict({ + 'client_secret': '**REDACTED**', + 'host': '**REDACTED**', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'webostv', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'LG webOS TV MODEL', + 'unique_id': '**REDACTED**', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/webostv/snapshots/test_media_player.ambr b/tests/components/webostv/snapshots/test_media_player.ambr new file mode 100644 index 00000000000..9c097b166ec --- /dev/null +++ b/tests/components/webostv/snapshots/test_media_player.ambr @@ -0,0 +1,78 @@ +# serializer version: 1 +# name: test_command + dict({ + 'media_player.lg_webos_tv_model': dict({ + 'muted': False, + 'returnValue': True, + 'scenario': 'mastervolume_tv_speaker_ext', + 'volume': 1, + }), + }) +# --- +# name: test_entity_attributes + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'tv', + 'friendly_name': 'LG webOS TV MODEL', + 'is_volume_muted': False, + 'media_content_type': , + 'media_title': 'Channel 1', + 'sound_output': 'speaker', + 'source': 'Live TV', + 'source_list': list([ + 'Input01', + 'Input02', + 'Live TV', + ]), + 'supported_features': , + 'volume_level': 0.37, + }), + 'context': , + 'entity_id': 'media_player.lg_webos_tv_model', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_attributes.1 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'webostv', + 'some-fake-uuid', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'LG', + 'model': 'MODEL', + 'model_id': None, + 'name': 'LG webOS TV MODEL', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': '1234567890', + 'suggested_area': None, + 'sw_version': 'major.minor', + 'via_device_id': None, + }) +# --- +# name: test_select_sound_output + dict({ + 'media_player.lg_webos_tv_model': dict({ + 'method': 'setSystemSettings', + 'returnValue': True, + }), + }) +# --- diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 9b2983aab47..34ab39618d8 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -1,63 +1,48 @@ -"""Test the WebOS Tv config flow.""" - -import dataclasses -from unittest.mock import Mock +"""Test the LG webOS TV config flow.""" from aiowebostv import WebOsTvPairError import pytest from homeassistant import config_entries -from homeassistant.components import ssdp from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV_APP_ID from homeassistant.config_entries import SOURCE_SSDP -from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_UDN, + SsdpServiceInfo, +) from . import setup_webostv -from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME +from .const import ( + CLIENT_KEY, + FAKE_UUID, + HOST, + MOCK_APPS, + MOCK_INPUTS, + TV_MODEL, + TV_NAME, +) pytestmark = pytest.mark.usefixtures("mock_setup_entry") -MOCK_USER_CONFIG = { - CONF_HOST: HOST, - CONF_NAME: TV_NAME, -} +MOCK_USER_CONFIG = {CONF_HOST: HOST} -MOCK_DISCOVERY_INFO = ssdp.SsdpServiceInfo( +MOCK_DISCOVERY_INFO = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=f"http://{HOST}", upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "LG Webostv", - ssdp.ATTR_UPNP_UDN: f"uuid:{FAKE_UUID}", + ATTR_UPNP_FRIENDLY_NAME: f"[LG] webOS TV {TV_MODEL}", + ATTR_UPNP_UDN: f"uuid:{FAKE_UUID}", }, ) async def test_form(hass: HomeAssistant, client) -> None: - """Test we get the form.""" - assert client - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: config_entries.SOURCE_USER}, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_USER_CONFIG, - ) - await hass.async_block_till_done() - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "pairing" - + """Test successful user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, @@ -72,10 +57,10 @@ async def test_form(hass: HomeAssistant, client) -> None: result["flow_id"], user_input={} ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == TV_NAME + config_entry = result["result"] + assert config_entry.unique_id == FAKE_UUID @pytest.mark.parametrize( @@ -109,26 +94,52 @@ async def test_options_flow_live_tv_in_apps( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - result2 = await hass.config_entries.options.async_configure( + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SOURCES: ["Live TV", "Input01", "Input02"]}, ) - await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] -async def test_options_flow_cannot_retrieve(hass: HomeAssistant, client) -> None: - """Test options config flow cannot retrieve sources.""" +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (WebOsTvPairError, "error_pairing"), + (ConnectionResetError, "cannot_connect"), + ], +) +async def test_options_flow_errors( + hass: HomeAssistant, client, side_effect, error +) -> None: + """Test options config flow errors.""" entry = await setup_webostv(hass) - client.connect = Mock(side_effect=ConnectionRefusedError()) + client.connect.side_effect = side_effect result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "cannot_retrieve"} + assert result["errors"] == {"base": error} + + # recover + client.connect.side_effect = None + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=None, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result3 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SOURCES: ["Input01", "Input02"]}, + ) + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["data"][CONF_SOURCES] == ["Input01", "Input02"] async def test_form_cannot_connect(hass: HomeAssistant, client) -> None: @@ -139,14 +150,22 @@ async def test_form_cannot_connect(hass: HomeAssistant, client) -> None: data=MOCK_USER_CONFIG, ) - client.connect = Mock(side_effect=ConnectionRefusedError()) - result2 = await hass.config_entries.flow.async_configure( + client.connect.side_effect = ConnectionResetError + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - await hass.async_block_till_done() - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + # recover + client.connect.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TV_NAME async def test_form_pairexception(hass: HomeAssistant, client) -> None: @@ -157,20 +176,27 @@ async def test_form_pairexception(hass: HomeAssistant, client) -> None: data=MOCK_USER_CONFIG, ) - client.connect = Mock(side_effect=WebOsTvPairError("error")) - result2 = await hass.config_entries.flow.async_configure( + client.connect.side_effect = WebOsTvPairError + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - await hass.async_block_till_done() - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "error_pairing" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "error_pairing"} + + # recover + client.connect.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TV_NAME async def test_entry_already_configured(hass: HomeAssistant, client) -> None: """Test entry already configured.""" await setup_webostv(hass) - assert client result = await hass.config_entries.flow.async_init( DOMAIN, @@ -184,8 +210,6 @@ async def test_entry_already_configured(hass: HomeAssistant, client) -> None: async def test_form_ssdp(hass: HomeAssistant, client) -> None: """Test that the ssdp confirmation form is served.""" - assert client - result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVERY_INFO ) @@ -194,11 +218,18 @@ async def test_form_ssdp(hass: HomeAssistant, client) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TV_NAME + config_entry = result["result"] + assert config_entry.unique_id == FAKE_UUID + async def test_ssdp_in_progress(hass: HomeAssistant, client) -> None: """Test abort if ssdp paring is already in progress.""" - assert client - result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, @@ -209,55 +240,20 @@ async def test_ssdp_in_progress(hass: HomeAssistant, client) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "pairing" - result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVERY_INFO - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "already_in_progress" - - -async def test_ssdp_update_uuid(hass: HomeAssistant, client) -> None: - """Test that ssdp updates existing host entry uuid.""" - entry = await setup_webostv(hass, None) - assert client - assert entry.unique_id is None - + # Start another ssdp flow to make sure it aborts as already in progress result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVERY_INFO ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] - - -async def test_ssdp_not_update_uuid(hass: HomeAssistant, client) -> None: - """Test that ssdp not updates different host.""" - entry = await setup_webostv(hass, None) - assert client - assert entry.unique_id is None - - discovery_info = dataclasses.replace(MOCK_DISCOVERY_INFO) - discovery_info.ssdp_location = "http://1.2.3.5" - - result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "pairing" - assert entry.unique_id is None + assert result["reason"] == "already_in_progress" async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: """Test abort if uuid is already configured, verify host update.""" - entry = await setup_webostv(hass, MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:]) - assert client - assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] + entry = await setup_webostv(hass, MOCK_DISCOVERY_INFO.upnp[ATTR_UPNP_UDN][5:]) + assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ATTR_UPNP_UDN][5:] assert entry.data[CONF_HOST] == HOST result = await hass.config_entries.flow.async_init( @@ -269,11 +265,9 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - user_config = { - CONF_HOST: "new_host", - CONF_NAME: TV_NAME, - } + user_config = {CONF_HOST: "new_host"} + # Start another flow to make sure it aborts and updates host result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, @@ -288,19 +282,14 @@ async def test_form_abort_uuid_configured(hass: HomeAssistant, client) -> None: result["flow_id"], user_input={} ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" -async def test_reauth_successful( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_reauth_successful(hass: HomeAssistant, client) -> None: """Test that the reauthorization is successful.""" entry = await setup_webostv(hass) - assert client result = await entry.start_reauth_flow(hass) assert result["step_id"] == "reauth_confirm" @@ -311,7 +300,7 @@ async def test_reauth_successful( assert result["step_id"] == "reauth_confirm" assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY - monkeypatch.setattr(client, "client_key", "new_key") + client.client_key = "new_key" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -322,18 +311,15 @@ async def test_reauth_successful( @pytest.mark.parametrize( - ("side_effect", "reason"), + ("side_effect", "error"), [ (WebOsTvPairError, "error_pairing"), - (ConnectionRefusedError, "reauth_unsuccessful"), + (ConnectionResetError, "cannot_connect"), ], ) -async def test_reauth_errors( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch, side_effect, reason -) -> None: +async def test_reauth_errors(hass: HomeAssistant, client, side_effect, error) -> None: """Test reauthorization errors.""" entry = await setup_webostv(hass) - assert client result = await entry.start_reauth_flow(hass) assert result["step_id"] == "reauth_confirm" @@ -343,10 +329,93 @@ async def test_reauth_errors( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) + client.connect.side_effect = side_effect() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + client.connect.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == reason + assert result["reason"] == "reauth_successful" + + +async def test_reconfigure_successful(hass: HomeAssistant, client) -> None: + """Test that the reconfigure is successful.""" + entry = await setup_webostv(hass) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data[CONF_HOST] == "new_host" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (WebOsTvPairError, "error_pairing"), + (ConnectionResetError, "cannot_connect"), + ], +) +async def test_reconfigure_errors( + hass: HomeAssistant, client, side_effect, error +) -> None: + """Test reconfigure errors.""" + entry = await setup_webostv(hass) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + client.connect.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + client.connect.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_wrong_device(hass: HomeAssistant, client) -> None: + """Test abort if reconfigure host is wrong webOS TV device.""" + entry = await setup_webostv(hass) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + client.hello_info = {"deviceUUID": "wrong_uuid"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "new_host"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device" diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index 41045969335..c14e8f4542a 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -1,4 +1,4 @@ -"""The tests for WebOS TV device triggers.""" +"""The tests for LG webOS TV device triggers.""" import pytest @@ -104,14 +104,14 @@ async def test_if_fires_on_turn_on_request( assert service_calls[2].data["id"] == 0 -async def test_failure_scenarios( +async def test_invalid_trigger_raises( hass: HomeAssistant, device_registry: dr.DeviceRegistry, client ) -> None: - """Test failure scenarios.""" + """Test invalid trigger platform or device id raises.""" await setup_webostv(hass) # Test wrong trigger platform type - with pytest.raises(HomeAssistantError): + with pytest.raises(HomeAssistantError, match="Unhandled trigger type: wrong.type"): await device_trigger.async_attach_trigger( hass, {"type": "wrong.type", "device_id": "invalid_device_id"}, None, {} ) @@ -128,7 +128,26 @@ async def test_failure_scenarios( }, ) - entry = MockConfigEntry(domain="fake", state=ConfigEntryState.LOADED, data={}) + +@pytest.mark.parametrize( + ("domain", "entry_state"), + [ + (DOMAIN, ConfigEntryState.NOT_LOADED), + ("fake", ConfigEntryState.LOADED), + ], +) +async def test_invalid_entry_raises( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client, + domain: str, + entry_state: ConfigEntryState, +) -> None: + """Test device id not loaded or from another domain raises.""" + await setup_webostv(hass) + + entry = MockConfigEntry(domain=domain, state=entry_state, data={}) + entry.runtime_data = None entry.add_to_hass(hass) device = device_registry.async_get_or_create( diff --git a/tests/components/webostv/test_diagnostics.py b/tests/components/webostv/test_diagnostics.py index 7f54e940966..0cf815ce9e2 100644 --- a/tests/components/webostv/test_diagnostics.py +++ b/tests/components/webostv/test_diagnostics.py @@ -1,6 +1,8 @@ -"""Tests for the diagnostics data provided by LG webOS Smart TV.""" +"""Tests for the diagnostics data provided by LG webOS TV.""" + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props -from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from . import setup_webostv @@ -10,57 +12,13 @@ from tests.typing import ClientSessionGenerator async def test_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator, client + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + client, + snapshot: SnapshotAssertion, ) -> None: """Test diagnostics.""" entry = await setup_webostv(hass) - assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == { - "client": { - "is_registered": True, - "is_connected": True, - "current_app_id": "com.webos.app.livetv", - "current_channel": { - "channelId": "ch1id", - "channelName": "Channel 1", - "channelNumber": "1", - }, - "apps": { - "com.webos.app.livetv": { - "icon": REDACTED, - "id": "com.webos.app.livetv", - "largeIcon": REDACTED, - "title": "Live TV", - } - }, - "inputs": { - "in1": {"appId": "app0", "id": "in1", "label": "Input01"}, - "in2": {"appId": "app1", "id": "in2", "label": "Input02"}, - }, - "system_info": {"modelName": "TVFAKE"}, - "software_info": {"major_ver": "major", "minor_ver": "minor"}, - "hello_info": {"deviceUUID": "**REDACTED**"}, - "sound_output": "speaker", - "is_on": True, - }, - "entry": { - "entry_id": entry.entry_id, - "version": 1, - "minor_version": 1, - "domain": "webostv", - "title": "fake_webos", - "data": { - "client_secret": "**REDACTED**", - "host": "**REDACTED**", - }, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "unique_id": REDACTED, - "disabled_by": None, - "created_at": entry.created_at.isoformat(), - "modified_at": entry.modified_at.isoformat(), - "discovery_keys": {}, - "subentries": [], - }, - } + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot( + exclude=props("created_at", "modified_at", "entry_id") + ) diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index e2638c86f5e..cd8f443c8fd 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -1,24 +1,21 @@ """The tests for the LG webOS TV platform.""" -from unittest.mock import Mock - from aiowebostv import WebOsTvPairError -import pytest -from homeassistant.components.webostv.const import DOMAIN +from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST +from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_CLIENT_SECRET +from homeassistant.const import CONF_CLIENT_SECRET, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from . import setup_webostv +from .const import ENTITY_ID -async def test_reauth_setup_entry( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_reauth_setup_entry(hass: HomeAssistant, client) -> None: """Test reauth flow triggered by setup entry.""" - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + client.is_connected.return_value = False + client.connect.side_effect = WebOsTvPairError entry = await setup_webostv(hass) assert entry.state is ConfigEntryState.SETUP_ERROR @@ -35,12 +32,46 @@ async def test_reauth_setup_entry( assert flow["context"].get("entry_id") == entry.entry_id -async def test_key_update_setup_entry( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_key_update_setup_entry(hass: HomeAssistant, client) -> None: """Test key update from setup entry.""" - monkeypatch.setattr(client, "client_key", "new_key") + client.client_key = "new_key" entry = await setup_webostv(hass) assert entry.state is ConfigEntryState.LOADED assert entry.data[CONF_CLIENT_SECRET] == "new_key" + + +async def test_update_options(hass: HomeAssistant, client) -> None: + """Test update options triggers reload.""" + config_entry = await setup_webostv(hass) + + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.update_listeners is not None + sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] + assert sources == ["Input01", "Input02", "Live TV"] + + # remove Input01 and reload + new_options = config_entry.options.copy() + new_options[CONF_SOURCES] = ["Input02", "Live TV"] + hass.config_entries.async_update_entry(config_entry, options=new_options) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] + assert sources == ["Input02", "Live TV"] + + +async def test_disconnect_on_stop(hass: HomeAssistant, client) -> None: + """Test we disconnect the client and clear callbacks when Home Assistants stops.""" + config_entry = await setup_webostv(hass) + + assert client.disconnect.call_count == 0 + assert client.clear_state_update_callbacks.call_count == 0 + assert config_entry.state is ConfigEntryState.LOADED + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert client.disconnect.call_count == 1 + assert client.clear_state_update_callbacks.call_count == 1 + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index e4c02e680bd..679092efe3b 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,11 +1,13 @@ -"""The tests for the LG webOS media player platform.""" +"""The tests for the LG webOS TV media player platform.""" from datetime import timedelta from http import HTTPStatus -from unittest.mock import Mock from aiowebostv import WebOsTvPairError +from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props from homeassistant.components import automation from homeassistant.components.media_player import ( @@ -19,7 +21,6 @@ from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, - MediaPlayerDeviceClass, MediaPlayerEntityFeature, MediaPlayerState, MediaType, @@ -42,9 +43,9 @@ from homeassistant.components.webostv.media_player import ( from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + CONF_CLIENT_SECRET, ENTITY_MATCH_NONE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, @@ -58,13 +59,11 @@ from homeassistant.const import ( SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util from . import setup_webostv from .const import CHANNEL_2, ENTITY_ID, TV_NAME @@ -144,7 +143,7 @@ async def test_media_play_pause(hass: HomeAssistant, client) -> None: ], ) async def test_media_next_previous_track( - hass: HomeAssistant, client, service, client_call, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, client, service, client_call ) -> None: """Test media next/previous track services.""" await setup_webostv(hass) @@ -157,7 +156,7 @@ async def test_media_next_previous_track( getattr(client, client_call[1]).assert_called_once() # check next/previous for not Live TV channels - monkeypatch.setattr(client, "current_app_id", "in1") + client.current_app_id = "in1" data = {ATTR_ENTITY_ID: ENTITY_ID} await hass.services.async_call(MP_DOMAIN, service, data, True) @@ -166,7 +165,7 @@ async def test_media_next_previous_track( async def test_select_source_with_empty_source_list( - hass: HomeAssistant, client, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, client ) -> None: """Ensure we don't call client methods when we don't have sources.""" await setup_webostv(hass) @@ -176,11 +175,14 @@ async def test_select_source_with_empty_source_list( ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "nonexistent", } - await hass.services.async_call(MP_DOMAIN, SERVICE_SELECT_SOURCE, data, True) + with pytest.raises( + HomeAssistantError, + match=f"Source nonexistent not found in the sources list for {TV_NAME}", + ): + await hass.services.async_call(MP_DOMAIN, SERVICE_SELECT_SOURCE, data, True) client.launch_app.assert_not_called() client.set_input.assert_not_called() - assert f"Source nonexistent not found for {TV_NAME}" in caplog.text async def test_select_app_source(hass: HomeAssistant, client) -> None: @@ -227,17 +229,30 @@ async def test_button(hass: HomeAssistant, client) -> None: client.button.assert_called_with("test") -async def test_command(hass: HomeAssistant, client) -> None: +async def test_command( + hass: HomeAssistant, + client, + snapshot: SnapshotAssertion, +) -> None: """Test generic command functionality.""" await setup_webostv(hass) + client.request.return_value = { + "returnValue": True, + "scenario": "mastervolume_tv_speaker_ext", + "volume": 1, + "muted": False, + } data = { ATTR_ENTITY_ID: ENTITY_ID, - ATTR_COMMAND: "test", + ATTR_COMMAND: "audio/getVolume", } - await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data, True) + response = await hass.services.async_call( + DOMAIN, SERVICE_COMMAND, data, True, return_response=True + ) await hass.async_block_till_done() - client.request.assert_called_with("test", payload=None) + client.request.assert_called_with("audio/getVolume", payload=None) + assert response == snapshot async def test_command_with_optional_arg(hass: HomeAssistant, client) -> None: @@ -256,28 +271,40 @@ async def test_command_with_optional_arg(hass: HomeAssistant, client) -> None: ) -async def test_select_sound_output(hass: HomeAssistant, client) -> None: +async def test_select_sound_output( + hass: HomeAssistant, + client, + snapshot: SnapshotAssertion, +) -> None: """Test select sound output service.""" await setup_webostv(hass) + client.change_sound_output.return_value = { + "returnValue": True, + "method": "setSystemSettings", + } data = { ATTR_ENTITY_ID: ENTITY_ID, ATTR_SOUND_OUTPUT: "external_speaker", } - await hass.services.async_call(DOMAIN, SERVICE_SELECT_SOUND_OUTPUT, data, True) + response = await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOUND_OUTPUT, + data, + True, + return_response=True, + ) await hass.async_block_till_done() client.change_sound_output.assert_called_once_with("external_speaker") + assert response == snapshot async def test_device_info_startup_off( - hass: HomeAssistant, - client, - monkeypatch: pytest.MonkeyPatch, - device_registry: dr.DeviceRegistry, + hass: HomeAssistant, client, device_registry: dr.DeviceRegistry ) -> None: """Test device info when device is off at startup.""" - monkeypatch.setattr(client, "system_info", None) - monkeypatch.setattr(client, "is_on", False) + client.system_info = None + client.is_on = False entry = await setup_webostv(hass) await client.mock_state_update() @@ -296,8 +323,8 @@ async def test_device_info_startup_off( async def test_entity_attributes( hass: HomeAssistant, client, - monkeypatch: pytest.MonkeyPatch, device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, ) -> None: """Test entity attributes.""" entry = await setup_webostv(hass) @@ -305,28 +332,17 @@ async def test_entity_attributes( # Attributes when device is on state = hass.states.get(ENTITY_ID) - attrs = state.attributes - - assert state.state == STATE_ON - assert state.name == TV_NAME - assert attrs[ATTR_DEVICE_CLASS] == MediaPlayerDeviceClass.TV - assert attrs[ATTR_MEDIA_VOLUME_MUTED] is False - assert attrs[ATTR_MEDIA_VOLUME_LEVEL] == 0.37 - assert attrs[ATTR_INPUT_SOURCE] == "Live TV" - assert attrs[ATTR_INPUT_SOURCE_LIST] == ["Input01", "Input02", "Live TV"] - assert attrs[ATTR_MEDIA_CONTENT_TYPE] == MediaType.CHANNEL - assert attrs[ATTR_MEDIA_TITLE] == "Channel 1" - assert attrs[ATTR_SOUND_OUTPUT] == "speaker" + assert state == snapshot(exclude=props("entity_picture")) # Volume level not available - monkeypatch.setattr(client, "volume", None) + client.volume = None await client.mock_state_update() attrs = hass.states.get(ENTITY_ID).attributes assert attrs.get(ATTR_MEDIA_VOLUME_LEVEL) is None # Channel change - monkeypatch.setattr(client, "current_channel", CHANNEL_2) + client.current_channel = CHANNEL_2 await client.mock_state_update() attrs = hass.states.get(ENTITY_ID).attributes @@ -334,17 +350,11 @@ async def test_entity_attributes( # Device Info device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) - - assert device - assert device.identifiers == {(DOMAIN, entry.unique_id)} - assert device.manufacturer == "LG" - assert device.name == TV_NAME - assert device.sw_version == "major.minor" - assert device.model == "TVFAKE" + assert device == snapshot # Sound output when off - monkeypatch.setattr(client, "sound_output", None) - monkeypatch.setattr(client, "is_on", False) + client.sound_output = None + client.is_on = False await client.mock_state_update() state = hass.states.get(ENTITY_ID) @@ -388,9 +398,7 @@ async def test_play_media(hass: HomeAssistant, client, media_id, ch_id) -> None: client.set_channel.assert_called_once_with(ch_id) -async def test_update_sources_live_tv_find( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_update_sources_live_tv_find(hass: HomeAssistant, client) -> None: """Test finding live TV app id in update sources.""" await setup_webostv(hass) await client.mock_state_update() @@ -402,14 +410,13 @@ async def test_update_sources_live_tv_find( assert len(sources) == 3 # Live TV is current app - apps = { + client.apps = { LIVE_TV_APP_ID: { "title": "Live TV", "id": "some_id", }, } - monkeypatch.setattr(client, "apps", apps) - monkeypatch.setattr(client, "current_app_id", "some_id") + client.current_app_id = "some_id" await client.mock_state_update() sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] @@ -417,14 +424,13 @@ async def test_update_sources_live_tv_find( assert len(sources) == 3 # Live TV is is in inputs - inputs = { + client.inputs = { LIVE_TV_APP_ID: { "label": "Live TV", "id": "some_id", "appId": LIVE_TV_APP_ID, }, } - monkeypatch.setattr(client, "inputs", inputs) await client.mock_state_update() sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] @@ -432,14 +438,13 @@ async def test_update_sources_live_tv_find( assert len(sources) == 1 # Live TV is current input - inputs = { + client.inputs = { LIVE_TV_APP_ID: { "label": "Live TV", "id": "some_id", "appId": "some_id", }, } - monkeypatch.setattr(client, "inputs", inputs) await client.mock_state_update() sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] @@ -447,7 +452,7 @@ async def test_update_sources_live_tv_find( assert len(sources) == 1 # Live TV not found - monkeypatch.setattr(client, "current_app_id", "other_id") + client.current_app_id = "other_id" await client.mock_state_update() sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] @@ -455,8 +460,8 @@ async def test_update_sources_live_tv_find( assert len(sources) == 1 # Live TV not found in sources/apps but is current app - monkeypatch.setattr(client, "apps", {}) - monkeypatch.setattr(client, "current_app_id", LIVE_TV_APP_ID) + client.apps = {} + client.current_app_id = LIVE_TV_APP_ID await client.mock_state_update() sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] @@ -464,7 +469,7 @@ async def test_update_sources_live_tv_find( assert len(sources) == 1 # Bad update, keep old update - monkeypatch.setattr(client, "inputs", {}) + client.inputs = {} await client.mock_state_update() sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] @@ -473,56 +478,95 @@ async def test_update_sources_live_tv_find( async def test_client_disconnected( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test error not raised when client is disconnected.""" - await setup_webostv(hass) - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=TimeoutError)) - - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) - await hass.async_block_till_done() - - -async def test_control_error_handling( hass: HomeAssistant, client, caplog: pytest.LogCaptureFixture, - monkeypatch: pytest.MonkeyPatch, + freezer: FrozenDateTimeFactory, +) -> None: + """Test error not raised when client is disconnected.""" + await setup_webostv(hass) + client.is_connected.return_value = False + client.connect.side_effect = TimeoutError + + freezer.tick(timedelta(seconds=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert "TimeoutError" not in caplog.text + + +async def test_client_key_update_on_connect( + hass: HomeAssistant, client, freezer: FrozenDateTimeFactory +) -> None: + """Test client key update upon connect.""" + config_entry = await setup_webostv(hass) + + assert config_entry.data[CONF_CLIENT_SECRET] == client.client_key + + client.is_connected.return_value = False + client.client_key = "new_key" + + freezer.tick(timedelta(seconds=20)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert config_entry.data[CONF_CLIENT_SECRET] == client.client_key + + +@pytest.mark.parametrize( + ("is_on", "exception", "error_message"), + [ + ( + True, + WebOsTvCommandError("Some error"), + f"Communication error while calling async_media_play for device {TV_NAME}: Some error", + ), + ( + True, + WebOsTvCommandError("Some other error"), + f"Communication error while calling async_media_play for device {TV_NAME}: Some other error", + ), + ( + False, + None, + f"Error calling async_media_play for device {TV_NAME}: Device is off and cannot be controlled", + ), + ], +) +async def test_control_error_handling( + hass: HomeAssistant, + client, + is_on: bool, + exception: Exception, + error_message: str, ) -> None: """Test control errors handling.""" await setup_webostv(hass) - monkeypatch.setattr(client, "play", Mock(side_effect=WebOsTvCommandError)) - data = {ATTR_ENTITY_ID: ENTITY_ID} + client.play.side_effect = exception + client.is_on = is_on + await client.mock_state_update() - # Device on, raise HomeAssistantError - with pytest.raises(HomeAssistantError) as exc: + data = {ATTR_ENTITY_ID: ENTITY_ID} + with pytest.raises(HomeAssistantError, match=error_message): await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True) - assert ( - str(exc.value) - == f"Error calling async_media_play on entity {ENTITY_ID}, state:on" - ) - assert client.play.call_count == 1 + assert client.play.call_count == int(is_on) - # Device off, log a warning - monkeypatch.setattr(client, "is_on", False) - monkeypatch.setattr(client, "play", Mock(side_effect=TimeoutError)) + +async def test_turn_off_when_device_is_off(hass: HomeAssistant, client) -> None: + """Test no error when turning off device that is already off.""" + await setup_webostv(hass) + client.is_on = False await client.mock_state_update() - await hass.services.async_call(MP_DOMAIN, SERVICE_MEDIA_PLAY, data, True) - assert client.play.call_count == 1 - assert ( - f"Error calling async_media_play on entity {ENTITY_ID}, state:off, error:" - " TimeoutError()" in caplog.text - ) + data = {ATTR_ENTITY_ID: ENTITY_ID} + await hass.services.async_call(MP_DOMAIN, SERVICE_TURN_OFF, data, True) + assert client.power_off.call_count == 1 -async def test_supported_features( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_supported_features(hass: HomeAssistant, client) -> None: """Test test supported features.""" - monkeypatch.setattr(client, "sound_output", "lineout") + client.sound_output = "lineout" await setup_webostv(hass) await client.mock_state_update() @@ -533,7 +577,7 @@ async def test_supported_features( assert attrs[ATTR_SUPPORTED_FEATURES] == supported # Support volume mute, step - monkeypatch.setattr(client, "sound_output", "external_speaker") + client.sound_output = "external_speaker" await client.mock_state_update() supported = supported | SUPPORT_WEBOSTV_VOLUME attrs = hass.states.get(ENTITY_ID).attributes @@ -541,7 +585,7 @@ async def test_supported_features( assert attrs[ATTR_SUPPORTED_FEATURES] == supported # Support volume mute, step, set - monkeypatch.setattr(client, "sound_output", "speaker") + client.sound_output = "speaker" await client.mock_state_update() supported = supported | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.VOLUME_SET attrs = hass.states.get(ENTITY_ID).attributes @@ -577,12 +621,10 @@ async def test_supported_features( assert attrs[ATTR_SUPPORTED_FEATURES] == supported -async def test_cached_supported_features( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_cached_supported_features(hass: HomeAssistant, client) -> None: """Test test supported features.""" - monkeypatch.setattr(client, "is_on", False) - monkeypatch.setattr(client, "sound_output", None) + client.is_on = False + client.sound_output = None supported = ( SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME | MediaPlayerEntityFeature.TURN_ON ) @@ -610,8 +652,8 @@ async def test_cached_supported_features( ) # TV on, support volume mute, step - monkeypatch.setattr(client, "is_on", True) - monkeypatch.setattr(client, "sound_output", "external_speaker") + client.is_on = True + client.sound_output = "external_speaker" await client.mock_state_update() supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME @@ -620,8 +662,8 @@ async def test_cached_supported_features( assert attrs[ATTR_SUPPORTED_FEATURES] == supported # TV off, support volume mute, step - monkeypatch.setattr(client, "is_on", False) - monkeypatch.setattr(client, "sound_output", None) + client.is_on = False + client.sound_output = None await client.mock_state_update() supported = SUPPORT_WEBOSTV | SUPPORT_WEBOSTV_VOLUME @@ -630,8 +672,8 @@ async def test_cached_supported_features( assert attrs[ATTR_SUPPORTED_FEATURES] == supported # TV on, support volume mute, step, set - monkeypatch.setattr(client, "is_on", True) - monkeypatch.setattr(client, "sound_output", "speaker") + client.is_on = True + client.sound_output = "speaker" await client.mock_state_update() supported = ( @@ -642,8 +684,8 @@ async def test_cached_supported_features( assert attrs[ATTR_SUPPORTED_FEATURES] == supported # TV off, support volume mute, step, set - monkeypatch.setattr(client, "is_on", False) - monkeypatch.setattr(client, "sound_output", None) + client.is_on = False + client.sound_output = None await client.mock_state_update() supported = ( @@ -684,12 +726,10 @@ async def test_cached_supported_features( ) -async def test_supported_features_no_cache( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_supported_features_no_cache(hass: HomeAssistant, client) -> None: """Test supported features if device is off and no cache.""" - monkeypatch.setattr(client, "is_on", False) - monkeypatch.setattr(client, "sound_output", None) + client.is_on = False + client.sound_output = None await setup_webostv(hass) supported = ( @@ -729,11 +769,10 @@ async def test_get_image_http( client, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - monkeypatch: pytest.MonkeyPatch, ) -> None: """Test get image via http.""" url = "http://something/valid_icon" - monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + client.apps[LIVE_TV_APP_ID]["icon"] = url await setup_webostv(hass) await client.mock_state_update() @@ -755,11 +794,10 @@ async def test_get_image_http_error( hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, - monkeypatch: pytest.MonkeyPatch, ) -> None: """Test get image via http error.""" url = "http://something/icon_error" - monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + client.apps[LIVE_TV_APP_ID]["icon"] = url await setup_webostv(hass) await client.mock_state_update() @@ -782,11 +820,10 @@ async def test_get_image_https( client, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - monkeypatch: pytest.MonkeyPatch, ) -> None: """Test get image via http.""" url = "https://something/valid_icon_https" - monkeypatch.setitem(client.apps[LIVE_TV_APP_ID], "icon", url) + client.apps[LIVE_TV_APP_ID]["icon"] = url await setup_webostv(hass) await client.mock_state_update() @@ -803,16 +840,17 @@ async def test_get_image_https( async def test_reauth_reconnect( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, client, freezer: FrozenDateTimeFactory ) -> None: """Test reauth flow triggered by reconnect.""" entry = await setup_webostv(hass) - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + client.is_connected.return_value = False + client.connect.side_effect = WebOsTvPairError assert entry.state is ConfigEntryState.LOADED - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) + freezer.tick(timedelta(seconds=20)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED @@ -829,27 +867,22 @@ async def test_reauth_reconnect( assert flow["context"].get("entry_id") == entry.entry_id -async def test_update_media_state( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_update_media_state(hass: HomeAssistant, client) -> None: """Test updating media state.""" await setup_webostv(hass) - data = {"foregroundAppInfo": [{"playState": "playing"}]} - monkeypatch.setattr(client, "media_state", data) + client.media_state = [{"playState": "playing"}] await client.mock_state_update() assert hass.states.get(ENTITY_ID).state == MediaPlayerState.PLAYING - data = {"foregroundAppInfo": [{"playState": "paused"}]} - monkeypatch.setattr(client, "media_state", data) + client.media_state = [{"playState": "paused"}] await client.mock_state_update() assert hass.states.get(ENTITY_ID).state == MediaPlayerState.PAUSED - data = {"foregroundAppInfo": [{"playState": "unloaded"}]} - monkeypatch.setattr(client, "media_state", data) + client.media_state = [{"playState": "unloaded"}] await client.mock_state_update() assert hass.states.get(ENTITY_ID).state == MediaPlayerState.IDLE - monkeypatch.setattr(client, "is_on", False) + client.is_on = False await client.mock_state_update() assert hass.states.get(ENTITY_ID).state == STATE_OFF diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index 75c2e148310..fd56f0ea0bb 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -1,8 +1,8 @@ -"""The tests for the WebOS TV notify platform.""" +"""The tests for the LG webOS TV notify platform.""" -from unittest.mock import Mock, call +from unittest.mock import call -from aiowebostv import WebOsTvPairError +from aiowebostv import WebOsTvCommandError import pytest from homeassistant.components.notify import ( @@ -13,23 +13,26 @@ from homeassistant.components.notify import ( from homeassistant.components.webostv import DOMAIN from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from homeassistant.util import slugify from . import setup_webostv from .const import TV_NAME ICON_PATH = "/some/path" MESSAGE = "one, two, testing, testing" +SERVICE_NAME = slugify(TV_NAME) async def test_notify(hass: HomeAssistant, client) -> None: """Test sending a message.""" await setup_webostv(hass) - assert hass.services.has_service(NOTIFY_DOMAIN, TV_NAME) + assert hass.services.has_service(NOTIFY_DOMAIN, SERVICE_NAME) await hass.services.async_call( NOTIFY_DOMAIN, - TV_NAME, + SERVICE_NAME, { ATTR_MESSAGE: MESSAGE, ATTR_DATA: { @@ -44,7 +47,7 @@ async def test_notify(hass: HomeAssistant, client) -> None: await hass.services.async_call( NOTIFY_DOMAIN, - TV_NAME, + SERVICE_NAME, { ATTR_MESSAGE: MESSAGE, ATTR_DATA: { @@ -59,7 +62,7 @@ async def test_notify(hass: HomeAssistant, client) -> None: await hass.services.async_call( NOTIFY_DOMAIN, - TV_NAME, + SERVICE_NAME, { ATTR_MESSAGE: "only message, no data", }, @@ -72,94 +75,54 @@ async def test_notify(hass: HomeAssistant, client) -> None: ) -async def test_notify_not_connected( - hass: HomeAssistant, client, monkeypatch: pytest.MonkeyPatch -) -> None: - """Test sending a message when client is not connected.""" - await setup_webostv(hass) - assert hass.services.has_service(NOTIFY_DOMAIN, TV_NAME) - - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - await hass.services.async_call( - NOTIFY_DOMAIN, - TV_NAME, - { - ATTR_MESSAGE: MESSAGE, - ATTR_DATA: { - ATTR_ICON: ICON_PATH, - }, - }, - blocking=True, - ) - assert client.mock_calls[0] == call.connect() - assert client.connect.call_count == 2 - client.send_message.assert_called_with(MESSAGE, icon_path=ICON_PATH) - - -async def test_icon_not_found( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - client, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test notify icon not found error.""" - await setup_webostv(hass) - assert hass.services.has_service(NOTIFY_DOMAIN, TV_NAME) - - monkeypatch.setattr(client, "send_message", Mock(side_effect=FileNotFoundError)) - await hass.services.async_call( - NOTIFY_DOMAIN, - TV_NAME, - { - ATTR_MESSAGE: MESSAGE, - ATTR_DATA: { - ATTR_ICON: ICON_PATH, - }, - }, - blocking=True, - ) - assert client.mock_calls[0] == call.connect() - assert client.connect.call_count == 1 - client.send_message.assert_called_with(MESSAGE, icon_path=ICON_PATH) - assert f"Icon {ICON_PATH} not found" in caplog.text - - @pytest.mark.parametrize( - ("side_effect", "error"), + ("is_on", "exception", "error_message"), [ - (WebOsTvPairError, "Pairing with TV failed"), - (ConnectionRefusedError, "TV unreachable"), + ( + True, + WebOsTvCommandError("Some error"), + f"Communication error while sending notification to device {TV_NAME}: Some error", + ), + ( + True, + FileNotFoundError("Some other error"), + f"Icon {ICON_PATH} not found when sending notification for device {TV_NAME}", + ), + ( + False, + None, + f"Error sending notification to device {TV_NAME}: Device is off and cannot be controlled", + ), ], ) -async def test_connection_errors( +async def test_errors( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, client, - monkeypatch: pytest.MonkeyPatch, - side_effect, - error, + is_on: bool, + exception: Exception, + error_message: str, ) -> None: - """Test connection errors scenarios.""" + """Test error scenarios.""" await setup_webostv(hass) - assert hass.services.has_service("notify", TV_NAME) + client.is_on = is_on - monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) - monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) - await hass.services.async_call( - NOTIFY_DOMAIN, - TV_NAME, - { - ATTR_MESSAGE: MESSAGE, - ATTR_DATA: { - ATTR_ICON: ICON_PATH, + assert hass.services.has_service("notify", SERVICE_NAME) + + client.send_message.side_effect = exception + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_NAME, + { + ATTR_MESSAGE: MESSAGE, + ATTR_DATA: { + ATTR_ICON: ICON_PATH, + }, }, - }, - blocking=True, - ) - assert client.mock_calls[0] == call.connect() - assert client.connect.call_count == 1 - client.send_message.assert_not_called() - assert error in caplog.text + blocking=True, + ) + + assert client.send_message.call_count == int(is_on) async def test_no_discovery_info( @@ -175,4 +138,4 @@ async def test_no_discovery_info( await hass.async_block_till_done() assert NOTIFY_DOMAIN in hass.config.components assert f"Failed to initialize notification service {DOMAIN}" in caplog.text - assert not hass.services.has_service("notify", TV_NAME) + assert not hass.services.has_service("notify", SERVICE_NAME) diff --git a/tests/components/webostv/test_trigger.py b/tests/components/webostv/test_trigger.py index d7eeae28ea3..c7decafff73 100644 --- a/tests/components/webostv/test_trigger.py +++ b/tests/components/webostv/test_trigger.py @@ -1,4 +1,4 @@ -"""The tests for WebOS TV automation triggers.""" +"""The tests for LG webOS TV automation triggers.""" from unittest.mock import patch @@ -118,10 +118,10 @@ async def test_webostv_turn_on_trigger_entity_id( assert service_calls[1].data["id"] == 0 -async def test_wrong_trigger_platform_type( +async def test_unknown_trigger_platform_type( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, client ) -> None: - """Test wrong trigger platform type.""" + """Test unknown trigger platform type.""" await setup_webostv(hass) await async_setup_component( @@ -131,7 +131,7 @@ async def test_wrong_trigger_platform_type( automation.DOMAIN: [ { "trigger": { - "platform": "webostv.wrong_type", + "platform": "webostv.unknown", "entity_id": ENTITY_ID, }, "action": { @@ -146,10 +146,7 @@ async def test_wrong_trigger_platform_type( }, ) - assert ( - "ValueError: Unknown webOS Smart TV trigger platform webostv.wrong_type" - in caplog.text - ) + assert "Unknown trigger platform: webostv.unknown" in caplog.text async def test_trigger_invalid_entity_id( @@ -185,7 +182,4 @@ async def test_trigger_invalid_entity_id( }, ) - assert ( - f"ValueError: Entity {invalid_entity} is not a valid webostv entity" - in caplog.text - ) + assert f"Entity {invalid_entity} is not a valid {DOMAIN} entity" in caplog.text diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index d55d2f97017..49ee593fed7 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -3,7 +3,7 @@ from unittest.mock import patch import aiohttp -from aiohttp import WSMsgType +from aiohttp import WSMsgType, web import pytest from homeassistant.auth.providers.homeassistant import HassAuthProvider @@ -258,7 +258,7 @@ async def test_auth_sending_binary_disconnects( await ws.send_bytes(b"[INVALID]") auth_msg = await ws.receive() - assert auth_msg.type == WSMsgType.close + assert auth_msg.type is WSMsgType.CLOSE async def test_auth_close_disconnects( @@ -277,7 +277,40 @@ async def test_auth_close_disconnects( await ws.close() auth_msg = await ws.receive() - assert auth_msg.type == WSMsgType.CLOSED + assert auth_msg.type is WSMsgType.CLOSED + + +async def test_auth_error_disconnects( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error during auth.""" + assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + ws_response = web.WebSocketResponse() + + with patch( + "homeassistant.components.websocket_api.http.web.WebSocketResponse", + return_value=ws_response, + ): + async with client.ws_connect(URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_REQUIRED + + ws_response._reader.feed_data( + aiohttp.WSMessage( + type=WSMsgType.ERROR, data=Exception("explode"), extra=None + ), + 0, + ) + + auth_msg = await ws.receive() + assert auth_msg.type is WSMsgType.CLOSE + + assert "Received error message during auth phase: explode" in caplog.text async def test_auth_sending_unknown_type_disconnects( @@ -296,3 +329,41 @@ async def test_auth_sending_unknown_type_disconnects( await ws._writer.send_frame(b"1" * 130, 0x30) auth_msg = await ws.receive() assert auth_msg.type == WSMsgType.close + + +async def test_error_right_after_auth_disconnects( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + hass_access_token: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error right after auth.""" + assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + ws_response = web.WebSocketResponse() + + with patch( + "homeassistant.components.websocket_api.http.web.WebSocketResponse", + return_value=ws_response, + ): + async with client.ws_connect(URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_REQUIRED + + await ws.send_json({"type": TYPE_AUTH, "access_token": hass_access_token}) + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_OK + + ws_response._reader.feed_data( + aiohttp.WSMessage( + type=WSMsgType.ERROR, data=Exception("explode"), extra=None + ), + 0, + ) + + close_error_msg = await ws.receive() + assert close_error_msg.type is WSMsgType.CLOSE + + assert "Received error message during command phase: explode" in caplog.text diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 22e839d84e4..2ddb5c628c7 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -460,10 +460,10 @@ async def test_call_service_child_not_found( "domain_test.test_service which was not found." ) assert msg["error"]["translation_placeholders"] == { - "domain": "non", - "service": "existing", - "child_domain": "domain_test", - "child_service": "test_service", + "domain": "domain_test", + "service": "test_service", + "child_domain": "non", + "child_service": "existing", } assert msg["error"]["translation_key"] == "child_service_not_found" assert msg["error"]["translation_domain"] == "websocket_api" diff --git a/tests/components/weheat/conftest.py b/tests/components/weheat/conftest.py index 6ecb64ffdf4..dbdeb0726dd 100644 --- a/tests/components/weheat/conftest.py +++ b/tests/components/weheat/conftest.py @@ -81,7 +81,7 @@ def mock_user_id() -> Generator[AsyncMock]: """Mock the user API call.""" with ( patch( - "homeassistant.components.weheat.config_flow.get_user_id_from_token", + "homeassistant.components.weheat.config_flow.async_get_user_id_from_token", return_value=USER_UUID_1, ) as user_mock, ): @@ -93,7 +93,7 @@ def mock_weheat_discover(mock_heat_pump_info) -> Generator[AsyncMock]: """Mock an Weheat discovery.""" with ( patch( - "homeassistant.components.weheat.HeatPumpDiscovery.discover_active", + "homeassistant.components.weheat.HeatPumpDiscovery.async_discover_active", autospec=True, ) as mock_discover, ): @@ -121,6 +121,14 @@ def mock_weheat_heat_pump_instance() -> MagicMock: mock_heat_pump_instance.cop = 4.5 mock_heat_pump_instance.heat_pump_state = HeatPump.State.HEATING mock_heat_pump_instance.energy_total = 12345 + mock_heat_pump_instance.energy_output = 56789 + mock_heat_pump_instance.compressor_rpm = 4500 + mock_heat_pump_instance.compressor_percentage = 100 + mock_heat_pump_instance.indoor_unit_water_pump_state = False + mock_heat_pump_instance.indoor_unit_auxiliary_pump_state = False + mock_heat_pump_instance.indoor_unit_dhw_valve_or_pump_state = None + mock_heat_pump_instance.indoor_unit_gas_boiler_state = False + mock_heat_pump_instance.indoor_unit_electric_heater_state = True return mock_heat_pump_instance diff --git a/tests/components/weheat/snapshots/test_binary_sensor.ambr b/tests/components/weheat/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..cd2aa13135a --- /dev/null +++ b/tests/components/weheat/snapshots/test_binary_sensor.ambr @@ -0,0 +1,192 @@ +# serializer version: 1 +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxilary_water_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_auxilary_water_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor unit auxilary water pump', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_auxiliary_pump_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_auxiliary_pump_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxilary_water_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Model Indoor unit auxilary water pump', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_auxilary_water_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_electric_heater-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_electric_heater', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor unit electric heater', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_electric_heater_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_electric_heater_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_electric_heater-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Model Indoor unit electric heater', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_electric_heater', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Indoor unit gas boiler heating allowed', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_gas_boiler_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_gas_boiler_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Model Indoor unit gas boiler heating allowed', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_gas_boiler_heating_allowed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_water_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_model_indoor_unit_water_pump', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Indoor unit water pump', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'indoor_unit_water_pump_state', + 'unique_id': '0000-1111-2222-3333_indoor_unit_water_pump_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_entities[binary_sensor.test_model_indoor_unit_water_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Model Indoor unit water pump', + }), + 'context': , + 'entity_id': 'binary_sensor.test_model_indoor_unit_water_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/weheat/snapshots/test_sensor.ambr b/tests/components/weheat/snapshots/test_sensor.ambr index 3bd4a254598..77f85224913 100644 --- a/tests/components/weheat/snapshots/test_sensor.ambr +++ b/tests/components/weheat/snapshots/test_sensor.ambr @@ -18,6 +18,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -78,6 +79,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -123,6 +125,108 @@ 'state': '33', }) # --- +# name: test_all_entities[sensor.test_model_compressor_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_model_compressor_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Compressor speed', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'compressor_rpm', + 'unique_id': '0000-1111-2222-3333_compressor_rpm', + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_all_entities[sensor.test_model_compressor_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Model Compressor speed', + 'state_class': , + 'unit_of_measurement': 'rpm', + }), + 'context': , + 'entity_id': 'sensor.test_model_compressor_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4500', + }) +# --- +# name: test_all_entities[sensor.test_model_compressor_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_model_compressor_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Compressor usage', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'compressor_percentage', + 'unique_id': '0000-1111-2222-3333_compressor_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.test_model_compressor_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Model Compressor usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_model_compressor_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- # name: test_all_entities[sensor.test_model_cop-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -132,6 +236,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -184,6 +289,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -238,6 +344,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -292,6 +399,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -346,6 +454,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -397,6 +506,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -451,6 +561,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -505,6 +616,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -559,6 +671,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -604,6 +717,58 @@ 'state': '21', }) # --- +# name: test_all_entities[sensor.test_model_total_energy_output-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_model_total_energy_output', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy output', + 'platform': 'weheat', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_output', + 'unique_id': '0000-1111-2222-3333_energy_output', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.test_model_total_energy_output-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Test Model Total energy output', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_model_total_energy_output', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56789', + }) +# --- # name: test_all_entities[sensor.test_model_water_inlet_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -613,6 +778,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -667,6 +833,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -721,6 +888,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/weheat/test_binary_sensor.py b/tests/components/weheat/test_binary_sensor.py new file mode 100644 index 00000000000..5769fc9a1a8 --- /dev/null +++ b/tests/components/weheat/test_binary_sensor.py @@ -0,0 +1,50 @@ +"""Tests for the weheat sensor platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion +from weheat.abstractions.discovery import HeatPumpDiscovery + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.weheat.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_create_binary_entities( + hass: HomeAssistant, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_heat_pump_info: HeatPumpDiscovery.HeatPumpInfo, + mock_config_entry: MockConfigEntry, +) -> None: + """Test creating entities.""" + mock_weheat_discover.return_value = [mock_heat_pump_info] + + with patch("homeassistant.components.weheat.PLATFORMS", [Platform.BINARY_SENSOR]): + await setup_integration(hass, mock_config_entry) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 diff --git a/tests/components/weheat/test_config_flow.py b/tests/components/weheat/test_config_flow.py index b33dd0a8db8..45f2285fd03 100644 --- a/tests/components/weheat/test_config_flow.py +++ b/tests/components/weheat/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_flow( with ( patch( - "homeassistant.components.weheat.config_flow.get_user_id_from_token", + "homeassistant.components.weheat.config_flow.async_get_user_id_from_token", return_value=USER_UUID_1, ) as mock_weheat, ): @@ -89,7 +89,7 @@ async def test_duplicate_unique_id( with ( patch( - "homeassistant.components.weheat.config_flow.get_user_id_from_token", + "homeassistant.components.weheat.config_flow.async_get_user_id_from_token", return_value=USER_UUID_1, ), ): diff --git a/tests/components/weheat/test_init.py b/tests/components/weheat/test_init.py new file mode 100644 index 00000000000..af5e2b8411b --- /dev/null +++ b/tests/components/weheat/test_init.py @@ -0,0 +1,85 @@ +"""Tests for the weheat initialization.""" + +from http import HTTPStatus +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from weheat.abstractions.discovery import HeatPumpDiscovery + +from homeassistant.components.weheat import UnauthorizedException +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import ClientResponseError + + +@pytest.mark.usefixtures("setup_credentials") +async def test_setup( + hass: HomeAssistant, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_heat_pump_info: HeatPumpDiscovery.HeatPumpInfo, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Weheat setup.""" + mock_weheat_discover.return_value = [mock_heat_pump_info] + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.usefixtures("setup_credentials") +@pytest.mark.parametrize( + ("setup_exception", "expected_setup_state"), + [ + (HTTPStatus.BAD_REQUEST, ConfigEntryState.SETUP_ERROR), + (HTTPStatus.UNAUTHORIZED, ConfigEntryState.SETUP_ERROR), + (HTTPStatus.FORBIDDEN, ConfigEntryState.SETUP_ERROR), + (HTTPStatus.GATEWAY_TIMEOUT, ConfigEntryState.SETUP_RETRY), + ], +) +async def test_setup_fail( + hass: HomeAssistant, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_heat_pump_info: HeatPumpDiscovery.HeatPumpInfo, + mock_config_entry: MockConfigEntry, + setup_exception: Exception, + expected_setup_state: ConfigEntryState, +) -> None: + """Test the Weheat setup with invalid token setup.""" + with ( + patch( + "homeassistant.components.weheat.OAuth2Session.async_ensure_token_valid", + side_effect=ClientResponseError( + Mock(real_url="http://example.com"), None, status=setup_exception + ), + ), + ): + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is expected_setup_state + + +@pytest.mark.usefixtures("setup_credentials") +async def test_setup_fail_discover( + hass: HomeAssistant, + mock_weheat_discover: AsyncMock, + mock_weheat_heat_pump: AsyncMock, + mock_heat_pump_info: HeatPumpDiscovery.HeatPumpInfo, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Weheat setup with and error from the heat pump discovery.""" + mock_weheat_discover.side_effect = UnauthorizedException() + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/weheat/test_sensor.py b/tests/components/weheat/test_sensor.py index d9055addc67..f3eec282704 100644 --- a/tests/components/weheat/test_sensor.py +++ b/tests/components/weheat/test_sensor.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion from weheat.abstractions.discovery import HeatPumpDiscovery @@ -34,14 +33,13 @@ async def test_all_entities( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 12), (True, 14)]) +@pytest.mark.parametrize(("has_dhw", "nr_of_entities"), [(False, 15), (True, 17)]) async def test_create_entities( hass: HomeAssistant, mock_weheat_discover: AsyncMock, mock_weheat_heat_pump: AsyncMock, mock_heat_pump_info: HeatPumpDiscovery.HeatPumpInfo, mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, has_dhw: bool, nr_of_entities: int, ) -> None: diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index 50620b20b8b..c302922fe25 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -39,7 +39,12 @@ def fixture_brand(request: pytest.FixtureRequest) -> tuple[str, Brand]: @pytest.fixture(name="mock_auth_api") def fixture_mock_auth_api(): """Set up Auth fixture.""" - with mock.patch("homeassistant.components.whirlpool.Auth") as mock_auth: + with ( + mock.patch("homeassistant.components.whirlpool.Auth") as mock_auth, + mock.patch( + "homeassistant.components.whirlpool.config_flow.Auth", new=mock_auth + ), + ): mock_auth.return_value.do_auth = AsyncMock() mock_auth.return_value.is_access_token_valid.return_value = True yield mock_auth @@ -48,9 +53,15 @@ def fixture_mock_auth_api(): @pytest.fixture(name="mock_appliances_manager_api") def fixture_mock_appliances_manager_api(): """Set up AppliancesManager fixture.""" - with mock.patch( - "homeassistant.components.whirlpool.AppliancesManager" - ) as mock_appliances_manager: + with ( + mock.patch( + "homeassistant.components.whirlpool.AppliancesManager" + ) as mock_appliances_manager, + mock.patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager", + new=mock_appliances_manager, + ), + ): mock_appliances_manager.return_value.fetch_appliances = AsyncMock() mock_appliances_manager.return_value.aircons = [ {"SAID": MOCK_SAID1, "NAME": "TestZone"}, @@ -81,9 +92,15 @@ def fixture_mock_appliances_manager_laundry_api(): @pytest.fixture(name="mock_backend_selector_api") def fixture_mock_backend_selector_api(): """Set up BackendSelector fixture.""" - with mock.patch( - "homeassistant.components.whirlpool.BackendSelector" - ) as mock_backend_selector: + with ( + mock.patch( + "homeassistant.components.whirlpool.BackendSelector" + ) as mock_backend_selector, + mock.patch( + "homeassistant.components.whirlpool.config_flow.BackendSelector", + new=mock_backend_selector, + ), + ): yield mock_backend_selector diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 1240e1303e1..e01fbc07b51 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Whirlpool Sixth Sense config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import aiohttp -from aiohttp.client_exceptions import ClientConnectionError +import pytest +from whirlpool.auth import AccountLockedError from homeassistant import config_entries from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN @@ -19,7 +20,23 @@ CONFIG_INPUT = { } -async def test_form(hass: HomeAssistant, region, brand) -> None: +@pytest.fixture(name="mock_whirlpool_setup_entry") +def fixture_mock_whirlpool_setup_entry(): + """Set up async_setup_entry fixture.""" + with patch( + "homeassistant.components.whirlpool.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.mark.usefixtures("mock_auth_api", "mock_appliances_manager_api") +async def test_form( + hass: HomeAssistant, + region, + brand, + mock_backend_selector_api: MagicMock, + mock_whirlpool_setup_entry: MagicMock, +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -28,33 +45,10 @@ async def test_form(hass: HomeAssistant, region, brand) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with ( - patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), - patch( - "homeassistant.components.whirlpool.config_flow.BackendSelector" - ) as mock_backend_selector, - patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", - return_value=["test"], - ), - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, + ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" @@ -64,93 +58,82 @@ async def test_form(hass: HomeAssistant, region, brand) -> None: "region": region[0], "brand": brand[0], } - assert len(mock_setup_entry.mock_calls) == 1 - mock_backend_selector.assert_called_once_with(brand[1], region[1]) + assert len(mock_whirlpool_setup_entry.mock_calls) == 1 + mock_backend_selector_api.assert_called_once_with(brand[1], region[1]) -async def test_form_invalid_auth(hass: HomeAssistant, region, brand) -> None: +async def test_form_invalid_auth( + hass: HomeAssistant, region, brand, mock_auth_api: MagicMock +) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with ( - patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=False, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, - ) + + mock_auth_api.return_value.is_access_token_valid.return_value = False + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistant, region, brand) -> None: +@pytest.mark.usefixtures("mock_appliances_manager_api") +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (AccountLockedError, "account_locked"), + (aiohttp.ClientConnectionError, "cannot_connect"), + (TimeoutError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_auth_error( + hass: HomeAssistant, + exception: Exception, + expected_error: str, + region, + brand, + mock_auth_api: MagicMock, + mock_whirlpool_setup_entry: MagicMock, +) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.whirlpool.config_flow.Auth.do_auth", - side_effect=aiohttp.ClientConnectionError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT - | { - "region": region[0], - "brand": brand[0], - }, - ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} - -async def test_form_auth_timeout(hass: HomeAssistant, region, brand) -> None: - """Test we handle auth timeout error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + mock_auth_api.return_value.do_auth.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT + | { + "region": region[0], + "brand": brand[0], + }, ) - with patch( - "homeassistant.components.whirlpool.config_flow.Auth.do_auth", - side_effect=TimeoutError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT - | { - "region": region[0], - "brand": brand[0], - }, - ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} - -async def test_form_generic_auth_exception(hass: HomeAssistant, region, brand) -> None: - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + # Test that it succeeds after the error is cleared + mock_auth_api.return_value.do_auth.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) - with patch( - "homeassistant.components.whirlpool.config_flow.Auth.do_auth", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT - | { - "region": region[0], - "brand": brand[0], - }, - ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == { + "username": "test-username", + "password": "test-password", + "region": region[0], + "brand": brand[0], + } + assert len(mock_whirlpool_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("mock_auth_api", "mock_appliances_manager_api") async def test_form_already_configured(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( @@ -167,36 +150,23 @@ async def test_form_already_configured(hass: HomeAssistant, region, brand) -> No assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with ( - patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", - return_value=["test"], - ), - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT - | { - "region": region[0], - "brand": brand[0], - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT + | { + "region": region[0], + "brand": brand[0], + }, + ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "already_configured" -async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: +@pytest.mark.usefixtures("mock_auth_api") +async def test_no_appliances_flow( + hass: HomeAssistant, region, brand, mock_appliances_manager_api: MagicMock +) -> None: """Test we get an error with no appliances.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -205,27 +175,20 @@ async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with ( - patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, - ) - await hass.async_block_till_done() + mock_appliances_manager_api.return_value.aircons = [] + mock_appliances_manager_api.return_value.washer_dryers = [] + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "no_appliances"} +@pytest.mark.usefixtures( + "mock_auth_api", "mock_appliances_manager_api", "mock_whirlpool_setup_entry" +) async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( @@ -241,30 +204,10 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ), - patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", - return_value=["test"], - ), - patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, + ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -276,7 +219,10 @@ async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: } -async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> None: +@pytest.mark.usefixtures("mock_appliances_manager_api", "mock_whirlpool_setup_entry") +async def test_reauth_flow_invalid_auth( + hass: HomeAssistant, region, brand, mock_auth_api: MagicMock +) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( @@ -290,29 +236,34 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> Non assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ), - patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=False, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, - ) - await hass.async_block_till_done() + + mock_auth_api.return_value.is_access_token_valid.return_value = False + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, + ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_flow_connnection_error( - hass: HomeAssistant, region, brand +@pytest.mark.usefixtures("mock_appliances_manager_api", "mock_whirlpool_setup_entry") +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (AccountLockedError, "account_locked"), + (aiohttp.ClientConnectionError, "cannot_connect"), + (TimeoutError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_reauth_flow_auth_error( + hass: HomeAssistant, + exception: Exception, + expected_error: str, + region, + brand, + mock_auth_api: MagicMock, ) -> None: """Test a connection error reauth flow.""" @@ -329,25 +280,10 @@ async def test_reauth_flow_connnection_error( assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.do_auth", - side_effect=ClientConnectionError, - ), - patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=False, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, - ) - await hass.async_block_till_done() - + mock_auth_api.return_value.do_auth.side_effect = exception + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, + ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": expected_error} diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index f9d28e78a06..8f082ff6294 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, MagicMock import aiohttp +from whirlpool.auth import AccountLockedError from whirlpool.backendselector import Brand, Region from homeassistant.components.whirlpool.const import DOMAIN @@ -104,6 +105,18 @@ async def test_setup_auth_failed( assert entry.state is ConfigEntryState.SETUP_ERROR +async def test_setup_auth_account_locked( + hass: HomeAssistant, + mock_auth_api: MagicMock, + mock_aircon_api_instances: MagicMock, +) -> None: + """Test setup with failed auth due to account being locked.""" + mock_auth_api.return_value.do_auth.side_effect = AccountLockedError + entry = await init_integration(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_setup_fetch_appliances_failed( hass: HomeAssistant, mock_appliances_manager_api: MagicMock, diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 4310bc77ebf..b5b1dde1c3d 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -19,6 +19,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -49,6 +50,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -128,6 +131,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -181,6 +185,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -211,6 +216,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -260,6 +266,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -290,6 +297,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -339,6 +347,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -369,6 +378,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -417,6 +427,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -447,6 +458,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -495,6 +507,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -525,6 +538,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -573,6 +587,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -603,6 +618,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -651,6 +667,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -681,6 +698,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -730,6 +748,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index d58cc342745..d290bc347a9 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.whois.const import SCAN_INTERVAL from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index acaf2aef2a8..8b32d7b1633 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -2,19 +2,19 @@ from pywilight.const import DOMAIN -from homeassistant.components import ssdp -from homeassistant.components.ssdp import ( - ATTR_UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME, - ATTR_UPNP_MODEL_NUMBER, - ATTR_UPNP_SERIAL, -) from homeassistant.components.wilight.config_flow import ( CONF_MODEL_NAME, CONF_SERIAL_NUMBER, ) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -34,7 +34,7 @@ UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" UPNP_MANUFACTURER_NOT_WILIGHT = "Test" CONF_COMPONENTS = "components" -MOCK_SSDP_DISCOVERY_INFO_P_B = ssdp.SsdpServiceInfo( +MOCK_SSDP_DISCOVERY_INFO_P_B = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_LOCATION, @@ -46,7 +46,7 @@ MOCK_SSDP_DISCOVERY_INFO_P_B = ssdp.SsdpServiceInfo( }, ) -MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = ssdp.SsdpServiceInfo( +MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_LOCATION, @@ -58,7 +58,7 @@ MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTURER = ssdp.SsdpServiceInfo( }, ) -MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER = ssdp.SsdpServiceInfo( +MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTURER = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location=SSDP_LOCATION, diff --git a/tests/components/withings/snapshots/test_init.ambr b/tests/components/withings/snapshots/test_init.ambr index be221cad313..ec711def829 100644 --- a/tests/components/withings/snapshots/test_init.ambr +++ b/tests/components/withings/snapshots/test_init.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), @@ -35,6 +36,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ }), diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index cfecfb1e28e..543cba05e21 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -66,6 +67,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -120,6 +122,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -175,6 +178,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -225,6 +229,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -275,6 +280,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -326,6 +332,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -380,6 +387,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -427,6 +435,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -479,6 +488,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -530,6 +540,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -578,6 +589,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -631,6 +643,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -684,6 +697,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -731,6 +745,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -778,6 +793,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -825,6 +841,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -875,6 +892,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -927,6 +945,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -978,6 +997,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1032,6 +1052,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1086,6 +1107,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1140,6 +1162,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1194,6 +1217,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1248,6 +1272,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1302,6 +1327,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1356,6 +1382,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1410,6 +1437,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1464,6 +1492,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1518,6 +1547,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1572,6 +1602,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1626,6 +1657,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1679,6 +1711,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1729,6 +1762,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1783,6 +1817,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1834,6 +1869,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1889,6 +1925,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1938,6 +1975,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -1989,6 +2027,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2087,6 +2126,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2187,6 +2227,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2238,6 +2279,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2288,6 +2330,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2338,6 +2381,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2388,6 +2432,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2438,6 +2483,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2493,6 +2539,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2547,6 +2594,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2601,6 +2649,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2655,6 +2704,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2709,6 +2759,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2763,6 +2814,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2815,6 +2867,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2868,6 +2921,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2919,6 +2973,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -2970,6 +3025,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3021,6 +3077,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3075,6 +3132,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3125,6 +3183,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3174,6 +3233,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3223,6 +3283,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3278,6 +3339,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3328,6 +3390,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3378,6 +3441,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3429,6 +3493,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3479,6 +3544,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3530,6 +3596,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3581,6 +3648,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3632,6 +3700,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3684,6 +3753,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3730,6 +3800,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3778,6 +3849,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3828,6 +3900,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3878,6 +3951,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3929,6 +4003,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -3983,6 +4058,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 39c8340a78e..b61a54150e4 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -5,10 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.withings.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import setup_integration from .conftest import CLIENT_ID, USER_ID @@ -293,3 +294,65 @@ async def test_config_flow_with_invalid_credentials( assert result assert result["type"] is FlowResultType.ABORT assert result["reason"] == "oauth_error" + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_dhcp( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Check DHCP discovery.""" + + service_info = DhcpServiceInfo( + hostname="device", + ip="192.168.0.1", + macaddress="0024e4bd30de", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=service_info + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] is FlowResultType.EXTERNAL_STEP + assert result["url"] == ( + "https://account.withings.com/oauth2_user/authorize2?" + f"response_type=code&client_id={CLIENT_ID}&" + "redirect_uri=https://example.com/auth/external/callback&" + f"state={state}" + "&scope=user.info,user.metrics,user.activity,user.sleepevents" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + "https://wbsapi.withings.net/v2/oauth2", + json={ + "body": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "userid": 600, + }, + }, + ) + with patch( + "homeassistant.components.withings.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index e07e1f90cb4..d88af39488b 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -10,6 +10,7 @@ from aiohttp.hdrs import METH_HEAD from aiowithings import ( NotificationCategory, WithingsAuthenticationFailedError, + WithingsConnectionError, WithingsUnauthorizedError, ) from freezegun.api import FrozenDateTimeFactory @@ -532,6 +533,59 @@ async def test_cloud_disconnect_retry( assert mock_async_active_subscription.call_count == 4 +async def test_internet_timeout_then_restore( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we can recover from internet disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert withings.revoke_notification_configurations.call_count == 3 + assert withings.subscribe_notification.call_count == 6 + + await hass.async_block_till_done() + + withings.list_notification_configurations.side_effect = WithingsConnectionError + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert withings.revoke_notification_configurations.call_count == 3 + withings.list_notification_configurations.side_effect = None + + async_mock_cloud_connection_status(hass, True) + await hass.async_block_till_done() + + assert withings.subscribe_notification.call_count == 12 + + @pytest.mark.parametrize( ("body", "expected_code"), [ diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index c60e080f6d4..ddf4a4f452a 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -6,13 +6,13 @@ import pytest from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError from homeassistant import config_entries -from homeassistant.components import dhcp from homeassistant.components.wiz.config_flow import CONF_DEVICE from homeassistant.components.wiz.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import ( FAKE_DIMMABLE_BULB, @@ -32,7 +32,7 @@ from . import ( from tests.common import MockConfigEntry -DHCP_DISCOVERY = dhcp.DhcpServiceInfo( +DHCP_DISCOVERY = DhcpServiceInfo( hostname="wiz_abcabc", ip=FAKE_IP, macaddress=FAKE_MAC, diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index 4e6260bc9bd..a22c1a3fb85 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -20,6 +20,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -50,6 +51,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 0fb6cff3d51..a99831d1440 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -28,6 +28,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -58,6 +59,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -119,6 +121,7 @@ 'step': 1, }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -149,6 +152,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 2998583f8b3..ca3b0a5dc6e 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -30,6 +30,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -60,6 +61,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -259,6 +261,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -289,6 +292,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -350,6 +354,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -380,6 +385,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -441,6 +447,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -471,6 +478,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index ee3a72ba872..99358153fe1 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -21,6 +21,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -51,6 +52,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -103,6 +105,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -133,6 +136,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -186,6 +190,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -216,6 +221,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( @@ -269,6 +275,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -299,6 +306,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://127.0.0.1', 'connections': set({ tuple( diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index a1cf515a24b..15db188af5e 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -6,12 +6,12 @@ from unittest.mock import AsyncMock, MagicMock import pytest from wled import WLEDConnectionError -from homeassistant.components import zeroconf from homeassistant.components.wled.const import CONF_KEEP_MAIN_LIGHT, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -43,7 +43,7 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -87,7 +87,7 @@ async def test_zeroconf_during_onboarding( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -132,7 +132,7 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -175,7 +175,7 @@ async def test_zeroconf_without_mac_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", @@ -200,7 +200,7 @@ async def test_zeroconf_with_mac_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.123"), ip_addresses=[ip_address("192.168.1.123")], hostname="example.local.", diff --git a/tests/components/wled/test_coordinator.py b/tests/components/wled/test_coordinator.py index 14e8b620983..e2935290f03 100644 --- a/tests/components/wled/test_coordinator.py +++ b/tests/components/wled/test_coordinator.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/wmspro/snapshots/test_cover.ambr b/tests/components/wmspro/snapshots/test_cover.ambr index 0456f074d49..53b2f6205cb 100644 --- a/tests/components/wmspro/snapshots/test_cover.ambr +++ b/tests/components/wmspro/snapshots/test_cover.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'terrasse', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://webcontrol/control', 'connections': set({ }), diff --git a/tests/components/wmspro/snapshots/test_light.ambr b/tests/components/wmspro/snapshots/test_light.ambr index d13e444645d..d6ccebfb5ea 100644 --- a/tests/components/wmspro/snapshots/test_light.ambr +++ b/tests/components/wmspro/snapshots/test_light.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'terrasse', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://webcontrol/control', 'connections': set({ }), diff --git a/tests/components/wmspro/snapshots/test_scene.ambr b/tests/components/wmspro/snapshots/test_scene.ambr index 940d4e31e83..b5dddb368c9 100644 --- a/tests/components/wmspro/snapshots/test_scene.ambr +++ b/tests/components/wmspro/snapshots/test_scene.ambr @@ -17,6 +17,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'raum_0', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://webcontrol/control', 'connections': set({ }), diff --git a/tests/components/wmspro/test_config_flow.py b/tests/components/wmspro/test_config_flow.py index 782dc051c8c..2c628bbc296 100644 --- a/tests/components/wmspro/test_config_flow.py +++ b/tests/components/wmspro/test_config_flow.py @@ -4,12 +4,12 @@ from unittest.mock import AsyncMock, patch import aiohttp -from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.wmspro.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import setup_config_entry diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index 1bf0f176fe9..51d4b899d25 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -653,3 +653,48 @@ async def test_form_with_categories(hass: HomeAssistant) -> None: "language": "de", "category": ["half_day"], } + + +async def test_options_form_removes_subdiv(hass: HomeAssistant) -> None: + """Test we get the form in options when removing a configured subdivision.""" + + entry = await init_integration( + hass, + { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "language": "de", + "province": "BW", + }, + ) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "language": "de", + }, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "name": "Workday Sensor", + "country": "DE", + "excludes": ["sat", "sun", "holiday"], + "days_offset": 0, + "workdays": ["mon", "tue", "wed", "thu", "fri"], + "add_holidays": [], + "remove_holidays": [], + "language": "de", + } diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index a8e3e41e649..4941462cb14 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -4,16 +4,10 @@ from datetime import tzinfo import pytest -from homeassistant.components.worldclock.const import ( - CONF_TIME_FORMAT, - DEFAULT_NAME, - DOMAIN, -) +from homeassistant.components.worldclock.const import CONF_TIME_FORMAT, DEFAULT_NAME from homeassistant.const import CONF_NAME, CONF_TIME_ZONE -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.helpers import issue_registry as ir -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -24,31 +18,6 @@ async def time_zone() -> tzinfo | None: return await dt_util.async_get_time_zone("America/New_York") -async def test_time_imported_from_yaml( - hass: HomeAssistant, time_zone: tzinfo | None, issue_registry: ir.IssueRegistry -) -> None: - """Test the time at a different location.""" - config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}} - - assert await async_setup_component( - hass, - "sensor", - config, - ) - await hass.async_block_till_done() - - state = hass.states.get("sensor.worldclock_sensor") - assert state is not None - - assert state.state == dt_util.now(time_zone=time_zone).strftime("%H:%M") - - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" - ) - assert issue - assert issue.issue_domain == DOMAIN - - async def test_time_from_config_entry( hass: HomeAssistant, time_zone: tzinfo | None, loaded_entry: MockConfigEntry ) -> None: diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index 9f5ec92a5b6..ff3d4960735 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -5,7 +5,7 @@ import re import requests_mock -import homeassistant.components.wsdot.sensor as wsdot +from homeassistant.components.wsdot import sensor as wsdot from homeassistant.components.wsdot.sensor import ( ATTR_DESCRIPTION, ATTR_TIME_UPDATED, diff --git a/tests/components/wyoming/test_config_flow.py b/tests/components/wyoming/test_config_flow.py index 6bca226d621..30faa2dd441 100644 --- a/tests/components/wyoming/test_config_flow.py +++ b/tests/components/wyoming/test_config_flow.py @@ -9,10 +9,10 @@ from wyoming.info import Info from homeassistant import config_entries from homeassistant.components.wyoming.const import DOMAIN -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from . import EMPTY_INFO, SATELLITE_INFO, STT_INFO, TTS_INFO @@ -178,11 +178,11 @@ async def test_hassio_addon_discovery( async def test_hassio_addon_already_configured(hass: HomeAssistant) -> None: """Test we abort discovery if the add-on is already configured.""" - MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, - data={"host": "mock-piper", "port": "10200"}, - unique_id="1234", - ).add_to_hass(hass) + data={"host": "mock-piper", "port": 10200}, + ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -191,6 +191,7 @@ async def test_hassio_addon_already_configured(hass: HomeAssistant) -> None: ) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "already_configured" + assert entry.unique_id == "1234" async def test_hassio_addon_cannot_connect(hass: HomeAssistant) -> None: @@ -297,3 +298,29 @@ async def test_zeroconf_discovery_no_services( assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "no_services" + + +async def test_zeroconf_discovery_already_configured( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test config flow initiated by Supervisor.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "127.0.0.1", "port": 12345}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ZEROCONF_DISCOVERY, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + + assert result.get("type") is FlowResultType.ABORT + assert entry.unique_id == "test_zeroconf_name._wyoming._tcp.local._Test Satellite" diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index f293f976242..0e4bb3da78c 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable import io +import tempfile from typing import Any from unittest.mock import patch import wave @@ -17,17 +18,18 @@ from wyoming.info import Info from wyoming.ping import Ping, Pong from wyoming.pipeline import PipelineStage, RunPipeline from wyoming.satellite import RunSatellite +from wyoming.snd import Played from wyoming.timer import TimerCancelled, TimerFinished, TimerStarted, TimerUpdated from wyoming.tts import Synthesize from wyoming.vad import VoiceStarted, VoiceStopped from wyoming.wake import Detect, Detection -from homeassistant.components import assist_pipeline, wyoming +from homeassistant.components import assist_pipeline, assist_satellite, wyoming from homeassistant.components.wyoming.assist_satellite import WyomingAssistSatellite from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, State -from homeassistant.helpers import intent as intent_helper +from homeassistant.helpers import entity_registry as er, intent as intent_helper from homeassistant.setup import async_setup_component from . import SATELLITE_INFO, WAKE_WORD_INFO, MockAsyncTcpClient @@ -65,7 +67,7 @@ def get_test_wav() -> bytes: wav_file.setnchannels(1) # Single frame - wav_file.writeframes(b"123") + wav_file.writeframes(b"1234") return wav_io.getvalue() @@ -73,10 +75,15 @@ def get_test_wav() -> bytes: class SatelliteAsyncTcpClient(MockAsyncTcpClient): """Satellite AsyncTcpClient.""" - def __init__(self, responses: list[Event]) -> None: + def __init__( + self, responses: list[Event], block_until_inject: bool = False + ) -> None: """Initialize client.""" super().__init__(responses) + self.block_until_inject = block_until_inject + self._responses_ready = asyncio.Event() + self.connect_event = asyncio.Event() self.run_satellite_event = asyncio.Event() self.detect_event = asyncio.Event() @@ -188,6 +195,9 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient): async def read_event(self) -> Event | None: """Receive.""" + if self.block_until_inject and (not self.responses): + await self._responses_ready.wait() + event = await super().read_event() # Keep sending audio chunks instead of None @@ -196,6 +206,7 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient): def inject_event(self, event: Event) -> None: """Put an event in as the next response.""" self.responses = [event, *self.responses] + self._responses_ready.set() async def test_satellite_pipeline(hass: HomeAssistant) -> None: @@ -416,7 +427,7 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: assert mock_client.tts_audio_chunk.rate == 22050 assert mock_client.tts_audio_chunk.width == 2 assert mock_client.tts_audio_chunk.channels == 1 - assert mock_client.tts_audio_chunk.audio == b"123" + assert mock_client.tts_audio_chunk.audio == b"1234" # Pipeline finished pipeline_event_callback( @@ -1283,3 +1294,85 @@ async def test_timers(hass: HomeAssistant) -> None: timer_finished = mock_client.timer_finished assert timer_finished is not None assert timer_finished.id == timer_started.id + + +async def test_announce( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test announce on satellite.""" + assert await async_setup_component(hass, assist_pipeline.DOMAIN, {}) + + def async_process_play_media_url(hass: HomeAssistant, media_id: str) -> str: + # Don't create a URL + return media_id + + with ( + tempfile.NamedTemporaryFile(mode="wb+", suffix=".wav") as temp_wav_file, + patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ), + patch( + "homeassistant.components.wyoming.assist_satellite.AsyncTcpClient", + SatelliteAsyncTcpClient(responses=[], block_until_inject=True), + ) as mock_client, + patch( + "homeassistant.components.assist_satellite.entity.async_process_play_media_url", + new=async_process_play_media_url, + ), + ): + # Use test WAV data for media + with wave.open(temp_wav_file.name, "wb") as wav_file: + wav_file.setframerate(22050) + wav_file.setsampwidth(2) + wav_file.setnchannels(1) + wav_file.writeframes(bytes(22050 * 2)) # 1 sec + + temp_wav_file.seek(0) + + entry = await setup_config_entry(hass) + device: SatelliteDevice = hass.data[wyoming.DOMAIN][entry.entry_id].device + assert device is not None + + satellite_entry = next( + ( + maybe_entry + for maybe_entry in er.async_entries_for_device( + entity_registry, device.device_id + ) + if maybe_entry.domain == assist_satellite.DOMAIN + ), + None, + ) + assert satellite_entry is not None + + async with asyncio.timeout(1): + await mock_client.connect_event.wait() + await mock_client.run_satellite_event.wait() + + announce_task = hass.async_create_background_task( + hass.services.async_call( + assist_satellite.DOMAIN, + "announce", + { + "entity_id": satellite_entry.entity_id, + "media_id": temp_wav_file.name, + }, + blocking=True, + ), + "wyoming_satellite_announce", + ) + + # Wait for audio to come from ffmpeg + async with asyncio.timeout(1): + await mock_client.tts_audio_start_event.wait() + await mock_client.tts_audio_chunk_event.wait() + await mock_client.tts_audio_stop_event.wait() + + # Stop announcement from blocking + mock_client.inject_event(Played().event()) + await announce_task + + # Stop the satellite + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 625e6f404ad..e3cc1898ce9 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, call, patch import requests from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -import homeassistant.components.xiaomi.device_tracker as xiaomi +from homeassistant.components.xiaomi import device_tracker as xiaomi from homeassistant.components.xiaomi.device_tracker import get_scanner from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 141e245815e..eb5cf976cb8 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -7,11 +7,11 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" @@ -409,7 +409,7 @@ async def test_zeroconf_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", @@ -456,7 +456,7 @@ async def test_zeroconf_missing_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", @@ -476,7 +476,7 @@ async def test_zeroconf_unknown_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index e25ac939a53..3d8a4dab244 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -634,6 +634,38 @@ async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_replace_ignored_entry(hass: HomeAssistant) -> None: + """Test setup from service info can replace an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=LYWSDCGQ_SERVICE_INFO.address, + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Temperature/Humidity Sensor 9321 (LYWSDCGQ)" + assert result2["data"] == {} + assert result2["result"].unique_id == "58:2D:34:35:93:21" + + async def test_async_step_user_short_payload(hass: HomeAssistant) -> None: """Test setup from service info cache with devices found but short payloads.""" with patch( diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 218a382ada5..f415a968f25 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -52,7 +52,7 @@ async def test_event_button_press(hass: HomeAssistant) -> None: hass, make_advertisement( mac, - b'XY\x97\td\xbc\x9c\xe3D\xefT" `' b"\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3", + b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3', ), ) @@ -78,7 +78,7 @@ async def test_event_unlock_outside_the_door(hass: HomeAssistant) -> None: hass, make_advertisement( mac, - b"PD\x9e\x06C\x91\x8a\xebD\x1f\xd7\x0b\x00\t" b" \x02\x00\x01\x80|D/a", + b"PD\x9e\x06C\x91\x8a\xebD\x1f\xd7\x0b\x00\t \x02\x00\x01\x80|D/a", ), ) @@ -104,7 +104,7 @@ async def test_event_successful_fingerprint_match_the_door(hass: HomeAssistant) hass, make_advertisement( mac, - b"PD\x9e\x06B\x91\x8a\xebD\x1f\xd7" b"\x06\x00\x05\xff\xff\xff\xff\x00", + b"PD\x9e\x06B\x91\x8a\xebD\x1f\xd7\x06\x00\x05\xff\xff\xff\xff\x00", ), ) @@ -153,7 +153,7 @@ async def test_event_dimmer_rotate(hass: HomeAssistant) -> None: inject_bluetooth_service_info_bleak( hass, make_advertisement( - mac, b"X0\xb6\x036\x8b\x98\xc5A$\xf8\x8b\xb8\xf2f" b"\x13Q\x00\x00\x00\xd6" + mac, b"X0\xb6\x036\x8b\x98\xc5A$\xf8\x8b\xb8\xf2f\x13Q\x00\x00\x00\xd6" ), ) @@ -182,7 +182,7 @@ async def test_get_triggers_button( hass, make_advertisement( mac, - b'XY\x97\td\xbc\x9c\xe3D\xefT" `' b"\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3", + b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3', ), ) @@ -406,7 +406,7 @@ async def test_if_fires_on_button_press( hass, make_advertisement( mac, - b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01\x08\x12\x05\x00\x00\x00q^\xbe\x90", ), ) @@ -442,7 +442,7 @@ async def test_if_fires_on_button_press( hass, make_advertisement( mac, - b'XY\x97\td\xbc\x9c\xe3D\xefT" `' b"\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3", + b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3', ), ) await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_event.py b/tests/components/xiaomi_ble/test_event.py index 1de5859c35e..7f31fe048aa 100644 --- a/tests/components/xiaomi_ble/test_event.py +++ b/tests/components/xiaomi_ble/test_event.py @@ -23,8 +23,7 @@ from tests.components.bluetooth import ( "54:EF:44:E3:9C:BC", make_advertisement( "54:EF:44:E3:9C:BC", - b'XY\x97\td\xbc\x9c\xe3D\xefT" `' - b"\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3", + b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3', ), "5b51a7c91cde6707c9ef18dfda143a58", [ @@ -114,7 +113,7 @@ from tests.components.bluetooth import ( "F8:24:41:C5:98:8B", make_advertisement( "F8:24:41:C5:98:8B", - b"X0\xb6\x036\x8b\x98\xc5A$\xf8\x8b\xb8\xf2f" b"\x13Q\x00\x00\x00\xd6", + b"X0\xb6\x036\x8b\x98\xc5A$\xf8\x8b\xb8\xf2f\x13Q\x00\x00\x00\xd6", ), "b853075158487ca39a5b5ea9", [ @@ -221,7 +220,7 @@ async def test_xiaomi_fingerprint(hass: HomeAssistant) -> None: hass, make_advertisement( "D7:1F:44:EB:8A:91", - b"PD\x9e\x06B\x91\x8a\xebD\x1f\xd7" b"\x06\x00\x05\xff\xff\xff\xff\x00", + b"PD\x9e\x06B\x91\x8a\xebD\x1f\xd7\x06\x00\x05\xff\xff\xff\xff\x00", ), ) @@ -264,7 +263,7 @@ async def test_xiaomi_lock(hass: HomeAssistant) -> None: hass, make_advertisement( "D7:1F:44:EB:8A:91", - b"PD\x9e\x06C\x91\x8a\xebD\x1f\xd7\x0b\x00\t" b" \x02\x00\x01\x80|D/a", + b"PD\x9e\x06C\x91\x8a\xebD\x1f\xd7\x0b\x00\t \x02\x00\x01\x80|D/a", ), ) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 146526c69a5..92fe53a8fc7 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -9,11 +9,11 @@ from miio import DeviceException import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import const from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from . import TEST_MAC @@ -434,7 +434,7 @@ async def test_zeroconf_gateway_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", @@ -477,7 +477,7 @@ async def test_zeroconf_unknown_device(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", @@ -497,7 +497,7 @@ async def test_zeroconf_no_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=None, ip_addresses=[], hostname="mock_hostname", @@ -517,7 +517,7 @@ async def test_zeroconf_missing_data(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", @@ -801,7 +801,7 @@ async def zeroconf_device_success( result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], hostname="mock_hostname", diff --git a/tests/components/yale/snapshots/test_binary_sensor.ambr b/tests/components/yale/snapshots/test_binary_sensor.ambr index e294cb7c76c..9db0d760efb 100644 --- a/tests/components/yale/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale/snapshots/test_binary_sensor.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'tmt100_name', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://account.aaecosystem.com', 'connections': set({ }), diff --git a/tests/components/yale/snapshots/test_lock.ambr b/tests/components/yale/snapshots/test_lock.ambr index b1a9f6a4d86..00653a9b0c1 100644 --- a/tests/components/yale/snapshots/test_lock.ambr +++ b/tests/components/yale/snapshots/test_lock.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'online_with_doorsense_name', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'https://account.aaecosystem.com', 'connections': set({ tuple( diff --git a/tests/components/yale/test_binary_sensor.py b/tests/components/yale/test_binary_sensor.py index 811c845e359..16ec0ffbeb4 100644 --- a/tests/components/yale/test_binary_sensor.py +++ b/tests/components/yale/test_binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .mocks import ( _create_yale_with_devices, diff --git a/tests/components/yale/test_event.py b/tests/components/yale/test_event.py index 7aeb9d8f12b..ce7f2635eea 100644 --- a/tests/components/yale/test_event.py +++ b/tests/components/yale/test_event.py @@ -4,7 +4,7 @@ from freezegun.api import FrozenDateTimeFactory from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .mocks import ( _create_yale_with_devices, diff --git a/tests/components/yale/test_lock.py b/tests/components/yale/test_lock.py index f6b96120d0d..1a99cf967ba 100644 --- a/tests/components/yale/test_lock.py +++ b/tests/components/yale/test_lock.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceNotSupported from homeassistant.helpers import device_registry as dr, entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .mocks import ( _create_yale_with_devices, diff --git a/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr b/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr index fcdb7baca03..daa232ab141 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr b/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr index e519a880de9..39b3ef09196 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -53,6 +54,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -100,6 +102,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -147,6 +150,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -194,6 +198,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -241,6 +246,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -288,6 +294,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -335,6 +342,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -382,6 +390,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -429,6 +438,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/yale_smart_alarm/snapshots/test_button.ambr b/tests/components/yale_smart_alarm/snapshots/test_button.ambr index 951caced170..7d52d1d7206 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_button.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_button.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/yale_smart_alarm/snapshots/test_lock.ambr b/tests/components/yale_smart_alarm/snapshots/test_lock.ambr index 34da7db087a..e7c97b9001b 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_lock.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_lock.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -54,6 +55,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -102,6 +104,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -150,6 +153,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -198,6 +202,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -246,6 +251,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/yale_smart_alarm/snapshots/test_select.ambr b/tests/components/yale_smart_alarm/snapshots/test_select.ambr index 52ec7a99c2c..2899e716ea1 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_select.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_select.ambr @@ -12,6 +12,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -69,6 +70,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -126,6 +128,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -183,6 +186,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -240,6 +244,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -297,6 +302,7 @@ ]), }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/yale_smart_alarm/snapshots/test_switch.ambr b/tests/components/yale_smart_alarm/snapshots/test_switch.ambr index f631a6fcbfe..17c44bf6ebf 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_switch.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_switch.ambr @@ -6,6 +6,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -52,6 +53,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -98,6 +100,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -144,6 +147,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -190,6 +194,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -236,6 +241,7 @@ 'area_id': None, 'capabilities': None, 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index c546e754239..1b0df05db2c 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -92,6 +92,58 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize("slot", [0, 1, 66]) +async def test_user_step_from_ignored(hass: HomeAssistant, slot: int) -> None: + """Test user step replaces an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + unique_id=YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: slot, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: slot, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: """Test user step with no devices found.""" with patch( diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 7629d2401c2..51645dee49e 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -7,12 +7,16 @@ from aiomusiccast import MusicCastConnectionException import pytest from homeassistant import config_entries -from homeassistant.components import ssdp from homeassistant.components.yamaha_musiccast.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) from tests.common import MockConfigEntry @@ -103,7 +107,7 @@ def mock_valid_discovery_information(): with patch( "homeassistant.components.ssdp.async_get_discovery_info_by_st", return_value=[ - ssdp.SsdpServiceInfo( + SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://127.0.0.1:9000/MediaRenderer/desc.xml", @@ -265,13 +269,13 @@ async def test_ssdp_discovery_failed(hass: HomeAssistant, mock_ssdp_no_yamaha) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://127.0.0.1/desc.xml", upnp={ - ssdp.ATTR_UPNP_MODEL_NAME: "MC20", - ssdp.ATTR_UPNP_SERIAL: "123456789", + ATTR_UPNP_MODEL_NAME: "MC20", + ATTR_UPNP_SERIAL: "123456789", }, ), ) @@ -287,13 +291,13 @@ async def test_ssdp_discovery_successful_add_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://127.0.0.1/desc.xml", upnp={ - ssdp.ATTR_UPNP_MODEL_NAME: "MC20", - ssdp.ATTR_UPNP_SERIAL: "1234567890", + ATTR_UPNP_MODEL_NAME: "MC20", + ATTR_UPNP_SERIAL: "1234567890", }, ), ) @@ -329,13 +333,13 @@ async def test_ssdp_discovery_existing_device_update( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://127.0.0.1/desc.xml", upnp={ - ssdp.ATTR_UPNP_MODEL_NAME: "MC20", - ssdp.ATTR_UPNP_SERIAL: "1234567890", + ATTR_UPNP_MODEL_NAME: "MC20", + ATTR_UPNP_SERIAL: "1234567890", }, ), ) diff --git a/tests/components/yandex_transport/test_sensor.py b/tests/components/yandex_transport/test_sensor.py index 13432850b2b..dd8e82278f3 100644 --- a/tests/components/yandex_transport/test_sensor.py +++ b/tests/components/yandex_transport/test_sensor.py @@ -10,7 +10,7 @@ from homeassistant.components import sensor from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import assert_setup_component, load_fixture diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index bdd8cdda312..f534b214b1c 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -9,7 +9,6 @@ from async_upnp_client.utils import CaseInsensitiveDict from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS -from homeassistant.components import zeroconf from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, @@ -21,6 +20,7 @@ from homeassistant.components.yeelight import ( ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME from homeassistant.core import callback +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo FAIL_TO_BIND_IP = "1.2.3.4" @@ -43,7 +43,7 @@ CAPABILITIES = { ID_DECIMAL = f"{int(ID, 16):08d}" -ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( +ZEROCONF_DATA = ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], port=54321, diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 1acb553af3d..a3f83cc03aa 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -6,7 +6,6 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.yeelight.config_flow import ( MODEL_UNKNOWN, CannotConnect, @@ -30,6 +29,12 @@ from homeassistant.components.yeelight.const import ( from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from . import ( CAPABILITIES, @@ -57,7 +62,7 @@ DEFAULT_CONFIG = { CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, } -SSDP_INFO = ssdp.SsdpServiceInfo( +SSDP_INFO = SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={}, @@ -493,13 +498,13 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, - data=zeroconf.ZeroconfServiceInfo( + data=ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ) @@ -525,7 +530,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=IP_ADDRESS, macaddress="aabbccddeeff", hostname="mock_hostname" ), ) @@ -543,7 +548,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip=IP_ADDRESS, macaddress="000000000000", hostname="mock_hostname" ), ) @@ -560,7 +565,7 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( ip="1.2.3.5", macaddress="000000000001", hostname="mock_hostname" ), ) @@ -574,19 +579,19 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress="aabbccddeeff", hostname="mock_hostname" ), ), ( config_entries.SOURCE_HOMEKIT, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ), @@ -648,19 +653,19 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress="aabbccddeeff", hostname="mock_hostname" ), ), ( config_entries.SOURCE_HOMEKIT, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ), @@ -894,19 +899,19 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant) -> None: [ ( config_entries.SOURCE_DHCP, - dhcp.DhcpServiceInfo( + DhcpServiceInfo( ip=IP_ADDRESS, macaddress="aabbccddeeff", hostname="mock_hostname" ), ), ( config_entries.SOURCE_HOMEKIT, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address(IP_ADDRESS), ip_addresses=[ip_address(IP_ADDRESS)], hostname="mock_hostname", name="mock_name", port=None, - properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + properties={ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, type="mock_type", ), ), diff --git a/tests/components/youless/__init__.py b/tests/components/youless/__init__.py index 8770a7e2dc8..03db24cb7f7 100644 --- a/tests/components/youless/__init__.py +++ b/tests/components/youless/__init__.py @@ -25,6 +25,11 @@ async def init_component(hass: HomeAssistant) -> MockConfigEntry: json=load_json_array_fixture("enologic.json", youless.DOMAIN), headers={"Content-Type": "application/json"}, ) + mock.get( + "http://1.1.1.1/f", + json=load_json_object_fixture("phase.json", youless.DOMAIN), + headers={"Content-Type": "application/json"}, + ) entry = MockConfigEntry( domain=youless.DOMAIN, diff --git a/tests/components/youless/fixtures/device.json b/tests/components/youless/fixtures/device.json index 7d089851923..82d07dba739 100644 --- a/tests/components/youless/fixtures/device.json +++ b/tests/components/youless/fixtures/device.json @@ -1,5 +1,5 @@ { "model": "LS120", - "fw": "1.4.2-EL", + "fw": "1.5.1-EL", "mac": "de2:2d2:3d23" } diff --git a/tests/components/youless/fixtures/phase.json b/tests/components/youless/fixtures/phase.json new file mode 100644 index 00000000000..8a5aa3215ef --- /dev/null +++ b/tests/components/youless/fixtures/phase.json @@ -0,0 +1,15 @@ +{ + "tr": 1, + "i1": 0.123, + "v1": 240, + "l1": 462, + "v2": 240, + "l2": 230, + "i2": 0.123, + "v3": 240, + "l3": 230, + "i3": 0.123, + "pp": 1200, + "pts": 2501301621, + "pa": 400 +} diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr index bcfd0139e5c..8cb28776d74 100644 --- a/tests/components/youless/snapshots/test_sensor.ambr +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensors[sensor.energy_delivery_high-entry] +# name: test_sensors[sensor.energy_delivery_meter_energy_export_tariff_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -8,13 +8,14 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.energy_delivery_high', - 'has_entity_name': False, + 'entity_id': 'sensor.energy_delivery_meter_energy_export_tariff_1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -25,83 +26,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Energy delivery high', + 'original_name': 'Energy export tariff 1', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_delivery_high', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.energy_delivery_high-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Energy delivery high', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.energy_delivery_high', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[sensor.energy_delivery_low-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.energy_delivery_low', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy delivery low', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'youless_localhost_delivery_low', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.energy_delivery_low-state] +# name: test_sensors[sensor.energy_delivery_meter_energy_export_tariff_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Energy delivery low', + 'friendly_name': 'Energy delivery meter Energy export tariff 1', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.energy_delivery_low', + 'entity_id': 'sensor.energy_delivery_meter_energy_export_tariff_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.029', }) # --- -# name: test_sensors[sensor.energy_high-entry] +# name: test_sensors[sensor.energy_delivery_meter_energy_export_tariff_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -110,13 +60,14 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.energy_high', - 'has_entity_name': False, + 'entity_id': 'sensor.energy_delivery_meter_energy_export_tariff_2', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -127,236 +78,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Energy high', + 'original_name': 'Energy export tariff 2', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_power_high', + 'translation_key': 'total_energy_export_tariff_kwh', + 'unique_id': 'youless_localhost_delivery_high', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.energy_high-state] +# name: test_sensors[sensor.energy_delivery_meter_energy_export_tariff_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Energy high', + 'friendly_name': 'Energy delivery meter Energy export tariff 2', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.energy_high', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4490.631', - }) -# --- -# name: test_sensors[sensor.energy_low-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.energy_low', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy low', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_power_low', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.energy_low-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Energy low', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.energy_low', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4703.562', - }) -# --- -# name: test_sensors[sensor.energy_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.energy_total', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Energy total', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_power_total', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.energy_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Energy total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.energy_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '9194.164', - }) -# --- -# name: test_sensors[sensor.extra_total-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.extra_total', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Extra total', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_extra_total', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.extra_total-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Extra total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.extra_total', + 'entity_id': 'sensor.energy_delivery_meter_energy_export_tariff_2', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '0.0', }) # --- -# name: test_sensors[sensor.extra_usage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.extra_usage', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Extra usage', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_extra_usage', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.extra_usage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Extra usage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.extra_usage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[sensor.gas_usage-entry] +# name: test_sensors[sensor.gas_meter_total_gas_usage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -365,13 +112,14 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.gas_usage', - 'has_entity_name': False, + 'entity_id': 'sensor.gas_meter_total_gas_usage', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -381,34 +129,33 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:fire', - 'original_name': 'Gas usage', + 'original_icon': None, + 'original_name': 'Total gas usage', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'total_gas_m3', 'unique_id': 'youless_localhost_gas', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.gas_usage-state] +# name: test_sensors[sensor.gas_meter_total_gas_usage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'gas', - 'friendly_name': 'Gas usage', - 'icon': 'mdi:fire', + 'friendly_name': 'Gas meter Total gas usage', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.gas_usage', + 'entity_id': 'sensor.gas_meter_total_gas_usage', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1624.264', }) # --- -# name: test_sensors[sensor.phase_1_current-entry] +# name: test_sensors[sensor.power_meter_average_peak-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -417,13 +164,66 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.phase_1_current', - 'has_entity_name': False, + 'entity_id': 'sensor.power_meter_average_peak', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Average peak', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'average_peak', + 'unique_id': 'youless_localhost_average_peak', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_average_peak-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power meter Average peak', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_average_peak', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '400', + }) +# --- +# name: test_sensors[sensor.power_meter_current_phase_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_current_phase_1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -434,32 +234,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase 1 current', + 'original_name': 'Current phase 1', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'active_current_phase_a', 'unique_id': 'youless_localhost_phase_1_current', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.phase_1_current-state] +# name: test_sensors[sensor.power_meter_current_phase_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'Phase 1 current', + 'friendly_name': 'Power meter Current phase 1', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.phase_1_current', + 'entity_id': 'sensor.power_meter_current_phase_1', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.123', }) # --- -# name: test_sensors[sensor.phase_1_power-entry] +# name: test_sensors[sensor.power_meter_current_phase_2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -468,115 +268,14 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.phase_1_power', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase 1 power', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_phase_1_power', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.phase_1_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Phase 1 power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.phase_1_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors[sensor.phase_1_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.phase_1_voltage', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase 1 voltage', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_phase_1_voltage', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.phase_1_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Phase 1 voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.phase_1_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors[sensor.phase_2_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.phase_2_current', - 'has_entity_name': False, + 'entity_id': 'sensor.power_meter_current_phase_2', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -587,32 +286,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase 2 current', + 'original_name': 'Current phase 2', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'active_current_phase_a', 'unique_id': 'youless_localhost_phase_2_current', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.phase_2_current-state] +# name: test_sensors[sensor.power_meter_current_phase_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'Phase 2 current', + 'friendly_name': 'Power meter Current phase 2', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.phase_2_current', + 'entity_id': 'sensor.power_meter_current_phase_2', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.123', }) # --- -# name: test_sensors[sensor.phase_2_power-entry] +# name: test_sensors[sensor.power_meter_current_phase_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -621,115 +320,14 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.phase_2_power', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase 2 power', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_phase_2_power', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.phase_2_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Phase 2 power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.phase_2_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors[sensor.phase_2_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.phase_2_voltage', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase 2 voltage', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_phase_2_voltage', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.phase_2_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Phase 2 voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.phase_2_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors[sensor.phase_3_current-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.phase_3_current', - 'has_entity_name': False, + 'entity_id': 'sensor.power_meter_current_phase_3', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -740,32 +338,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase 3 current', + 'original_name': 'Current phase 3', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'active_current_phase_a', 'unique_id': 'youless_localhost_phase_3_current', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.phase_3_current-state] +# name: test_sensors[sensor.power_meter_current_phase_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', - 'friendly_name': 'Phase 3 current', + 'friendly_name': 'Power meter Current phase 3', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.phase_3_current', + 'entity_id': 'sensor.power_meter_current_phase_3', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '0.123', }) # --- -# name: test_sensors[sensor.phase_3_power-entry] +# name: test_sensors[sensor.power_meter_current_power_usage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -774,13 +372,14 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.phase_3_power', - 'has_entity_name': False, + 'entity_id': 'sensor.power_meter_current_power_usage', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -791,134 +390,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Phase 3 power', + 'original_name': 'Current power usage', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_phase_3_power', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.phase_3_power-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Phase 3 power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.phase_3_power', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors[sensor.phase_3_voltage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.phase_3_voltage', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phase 3 voltage', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'youless_localhost_phase_3_voltage', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.phase_3_voltage-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Phase 3 voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.phase_3_voltage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unavailable', - }) -# --- -# name: test_sensors[sensor.power_usage-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.power_usage', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power Usage', - 'platform': 'youless', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'active_power_w', 'unique_id': 'youless_localhost_usage', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.power_usage-state] +# name: test_sensors[sensor.power_meter_current_power_usage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Power Usage', + 'friendly_name': 'Power meter Current power usage', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.power_usage', + 'entity_id': 'sensor.power_meter_current_power_usage', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '2382', }) # --- -# name: test_sensors[sensor.water_usage-entry] +# name: test_sensors[sensor.power_meter_energy_import_tariff_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -927,13 +424,695 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.water_usage', - 'has_entity_name': False, + 'entity_id': 'sensor.power_meter_energy_import_tariff_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy import tariff 1', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy_import_tariff_kwh', + 'unique_id': 'youless_localhost_power_low', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_energy_import_tariff_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Power meter Energy import tariff 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_energy_import_tariff_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4703.562', + }) +# --- +# name: test_sensors[sensor.power_meter_energy_import_tariff_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_energy_import_tariff_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy import tariff 2', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy_import_tariff_kwh', + 'unique_id': 'youless_localhost_power_high', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_energy_import_tariff_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Power meter Energy import tariff 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_energy_import_tariff_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4490.631', + }) +# --- +# name: test_sensors[sensor.power_meter_month_peak-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_month_peak', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Month peak', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'month_peak', + 'unique_id': 'youless_localhost_month_peak', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_month_peak-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power meter Month peak', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_month_peak', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1200', + }) +# --- +# name: test_sensors[sensor.power_meter_power_phase_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_power_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 1', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_power_phase_w', + 'unique_id': 'youless_localhost_phase_1_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_power_phase_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power meter Power phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_power_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '462', + }) +# --- +# name: test_sensors[sensor.power_meter_power_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_power_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 2', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_power_phase_w', + 'unique_id': 'youless_localhost_phase_2_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_power_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power meter Power phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_power_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_sensors[sensor.power_meter_power_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_power_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 3', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_power_phase_w', + 'unique_id': 'youless_localhost_phase_3_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_power_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Power meter Power phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_power_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_sensors[sensor.power_meter_tariff-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '1', + '2', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_tariff', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tariff', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_tariff', + 'unique_id': 'youless_localhost_tariff', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.power_meter_tariff-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Power meter Tariff', + 'options': list([ + '1', + '2', + ]), + }), + 'context': , + 'entity_id': 'sensor.power_meter_tariff', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensors[sensor.power_meter_total_energy_import-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_total_energy_import', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy import', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy_import_kwh', + 'unique_id': 'youless_localhost_power_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_total_energy_import-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Power meter Total energy import', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_total_energy_import', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9194.164', + }) +# --- +# name: test_sensors[sensor.power_meter_voltage_phase_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_voltage_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 1', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_voltage_phase_v', + 'unique_id': 'youless_localhost_phase_1_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_voltage_phase_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Power meter Voltage phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_voltage_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '240', + }) +# --- +# name: test_sensors[sensor.power_meter_voltage_phase_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_voltage_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 2', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_voltage_phase_v', + 'unique_id': 'youless_localhost_phase_2_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_voltage_phase_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Power meter Voltage phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_voltage_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '240', + }) +# --- +# name: test_sensors[sensor.power_meter_voltage_phase_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.power_meter_voltage_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage phase 3', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_voltage_phase_v', + 'unique_id': 'youless_localhost_phase_3_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.power_meter_voltage_phase_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Power meter Voltage phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.power_meter_voltage_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '240', + }) +# --- +# name: test_sensors[sensor.s0_meter_current_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.s0_meter_current_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current usage', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_s0_w', + 'unique_id': 'youless_localhost_extra_usage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.s0_meter_current_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'S0 meter Current usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.s0_meter_current_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.s0_meter_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.s0_meter_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'youless', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_s0_kwh', + 'unique_id': 'youless_localhost_extra_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.s0_meter_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'S0 meter Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.s0_meter_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.water_meter_total_water_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.water_meter_total_water_usage', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -943,27 +1122,26 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:water', - 'original_name': 'Water usage', + 'original_icon': None, + 'original_name': 'Total water usage', 'platform': 'youless', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'total_water', 'unique_id': 'youless_localhost_water', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.water_usage-state] +# name: test_sensors[sensor.water_meter_total_water_usage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'water', - 'friendly_name': 'Water usage', - 'icon': 'mdi:water', + 'friendly_name': 'Water meter Total water usage', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.water_usage', + 'entity_id': 'sensor.water_meter_total_water_usage', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/youless/test_init.py b/tests/components/youless/test_init.py index 29db8c66af0..9f0956cea35 100644 --- a/tests/components/youless/test_init.py +++ b/tests/components/youless/test_init.py @@ -15,4 +15,4 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: assert await setup.async_setup_component(hass, youless.DOMAIN, {}) assert entry.state is ConfigEntryState.LOADED - assert len(hass.states.async_entity_ids()) == 19 + assert len(hass.states.async_entity_ids()) == 22 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index be78964f231..56262600511 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -24,9 +24,18 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.generated import zeroconf as zc_gen from homeassistant.helpers.discovery_flow import DiscoveryKey +from homeassistant.helpers.service_info.zeroconf import ( + ATTR_PROPERTIES_ID, + ZeroconfServiceInfo, +) from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.common import ( + MockConfigEntry, + MockModule, + import_and_test_deprecated_constant, + mock_integration, +) NON_UTF8_VALUE = b"ABCDEF\x8a" NON_ASCII_KEY = b"non-ascii-key\x8a" @@ -1081,7 +1090,7 @@ async def test_async_detect_interfaces_setting_non_loopback_route( patch.object(hass.config_entries.flow, "async_init"), patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", + "homeassistant.components.zeroconf.network.async_get_loaded_adapters", return_value=_ADAPTER_WITH_DEFAULT_ENABLED, ), patch( @@ -1169,7 +1178,7 @@ async def test_async_detect_interfaces_setting_empty_route_linux( patch.object(hass.config_entries.flow, "async_init"), patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", + "homeassistant.components.zeroconf.network.async_get_loaded_adapters", return_value=_ADAPTERS_WITH_MANUAL_CONFIG, ), patch( @@ -1203,7 +1212,7 @@ async def test_async_detect_interfaces_setting_empty_route_freebsd( patch.object(hass.config_entries.flow, "async_init"), patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", + "homeassistant.components.zeroconf.network.async_get_loaded_adapters", return_value=_ADAPTERS_WITH_MANUAL_CONFIG, ), patch( @@ -1254,7 +1263,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux( patch.object(hass.config_entries.flow, "async_init"), patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", + "homeassistant.components.zeroconf.network.async_get_loaded_adapters", return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, ), patch( @@ -1283,7 +1292,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd( patch.object(hass.config_entries.flow, "async_init"), patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", + "homeassistant.components.zeroconf.network.async_get_loaded_adapters", return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, ), patch( @@ -1301,6 +1310,36 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd( ) +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_async_detect_interfaces_explicitly_before_setup( + hass: HomeAssistant, +) -> None: + """Test interfaces are explicitly set with IPv6 before setup is called.""" + with ( + patch("homeassistant.components.zeroconf.sys.platform", "linux"), + patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.network.async_get_loaded_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), + ): + # Call before async_setup has been called + await zeroconf.async_get_async_instance(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert mock_zc.mock_calls[0] == call( + interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef%3"], + ip_version=IPVersion.All, + ) + + async def test_no_name(hass: HomeAssistant, mock_async_zeroconf: MagicMock) -> None: """Test fallback to Home for mDNS announcement if the name is missing.""" hass.config.location_name = "" @@ -1655,3 +1694,35 @@ async def test_zeroconf_rediscover_no_match( assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("constant_name", "replacement_name", "replacement"), + [ + ( + "ATTR_PROPERTIES_ID", + "homeassistant.helpers.service_info.zeroconf.ATTR_PROPERTIES_ID", + ATTR_PROPERTIES_ID, + ), + ( + "ZeroconfServiceInfo", + "homeassistant.helpers.service_info.zeroconf.ZeroconfServiceInfo", + ZeroconfServiceInfo, + ), + ], +) +def test_deprecated_constants( + caplog: pytest.LogCaptureFixture, + constant_name: str, + replacement_name: str, + replacement: Any, +) -> None: + """Test deprecated automation constants.""" + import_and_test_deprecated_constant( + caplog, + zeroconf, + constant_name, + replacement_name, + replacement, + "2026.2", + ) diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 724414b5965..6cadc025385 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -29,7 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/zeversolar/snapshots/test_sensor.ambr b/tests/components/zeversolar/snapshots/test_sensor.ambr index aaef2c43d79..f948eec79df 100644 --- a/tests/components/zeversolar/snapshots/test_sensor.ambr +++ b/tests/components/zeversolar/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, @@ -59,6 +60,7 @@ 'state_class': , }), 'config_entry_id': , + 'config_subentry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 1dd1e5f81aa..89526f6431e 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -9,7 +9,7 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components.zha.helpers import ZHADeviceProxy from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 1b280ea499a..96a61a6628b 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -25,7 +25,7 @@ from zigpy.zcl.clusters.general import Basic, Groups from zigpy.zcl.foundation import Status import zigpy.zdo.types as zdo_t -import homeassistant.components.zha.const as zha_const +from homeassistant.components.zha import const as zha_const from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -155,6 +155,7 @@ async def zigpy_app_controller(): app.state.node_info.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") app.state.node_info.manufacturer = "Coordinator Manufacturer" app.state.node_info.model = "Coordinator Model" + app.state.node_info.version = "7.1.4.0 build 389" app.state.network_info.pan_id = 0x1234 app.state.network_info.extended_pan_id = app.state.node_info.ieee app.state.network_info.channel = 15 diff --git a/tests/components/zha/snapshots/test_diagnostics.ambr b/tests/components/zha/snapshots/test_diagnostics.ambr index 08807f65d5d..7a599b00a21 100644 --- a/tests/components/zha/snapshots/test_diagnostics.ambr +++ b/tests/components/zha/snapshots/test_diagnostics.ambr @@ -75,7 +75,7 @@ 'manufacturer': 'Coordinator Manufacturer', 'model': 'Coordinator Model', 'nwk': 0, - 'version': None, + 'version': '7.1.4.0 build 389', }), }), 'config': dict({ diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index e0229ebe049..94566be2f87 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -20,9 +20,7 @@ from zigpy.exceptions import NetworkNotFormed import zigpy.types from homeassistant import config_entries -from homeassistant.components import ssdp, usb, zeroconf from homeassistant.components.hassio import AddonError, AddonState -from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow, radio_manager from homeassistant.components.zha.const import ( CONF_BAUDRATE, @@ -43,6 +41,13 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.ssdp import ( + ATTR_UPNP_MANUFACTURER_URL, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.helpers.service_info.usb import UsbServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -162,7 +167,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: "tubeszb-cc2652-poe", "tubeszb-cc2652-poe", RadioType.znp, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="tubeszb-cc2652-poe.local.", @@ -175,7 +180,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: "network": "ethernet", "board": "esp32-poe", "platform": "ESP32", - "maс": "8c4b14c33c24", + "mac": "8c4b14c33c24", "version": "2023.12.8", }, ), @@ -185,7 +190,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: "tubeszb-efr32-poe", "tubeszb-efr32-poe", RadioType.ezsp, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="tubeszb-efr32-poe.local.", @@ -198,7 +203,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: "network": "ethernet", "board": "esp32-poe", "platform": "ESP32", - "maс": "8c4b14c33c24", + "mac": "8c4b14c33c24", "version": "2023.12.8", }, ), @@ -208,7 +213,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: "TubeZB", "tubeszb-cc2652-poe", RadioType.znp, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="tubeszb-cc2652-poe.local.", @@ -229,7 +234,7 @@ def com_port(device="/dev/ttyUSB1234") -> ListPortInfo: "Some Zigbee Gateway (12345)", "aabbccddeeff", RadioType.znp, - zeroconf.ZeroconfServiceInfo( + ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="some-zigbee-gateway-12345.local.", @@ -248,7 +253,7 @@ async def test_zeroconf_discovery( entry_name: str, unique_id: str, radio_type: RadioType, - service_info: zeroconf.ZeroconfServiceInfo, + service_info: ZeroconfServiceInfo, hass: HomeAssistant, ) -> None: """Test zeroconf flow -- radio detected.""" @@ -290,7 +295,7 @@ async def test_legacy_zeroconf_discovery_zigate( setup_entry_mock, hass: HomeAssistant ) -> None: """Test zeroconf flow -- zigate radio detected.""" - service_info = zeroconf.ZeroconfServiceInfo( + service_info = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="_zigate-zigbee-gateway.local.", @@ -339,7 +344,7 @@ async def test_legacy_zeroconf_discovery_zigate( async def test_zeroconf_discovery_bad_payload(hass: HomeAssistant) -> None: """Test zeroconf flow with a bad payload.""" - service_info = zeroconf.ZeroconfServiceInfo( + service_info = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="some.hostname", @@ -367,7 +372,7 @@ async def test_legacy_zeroconf_discovery_ip_change_ignored(hass: HomeAssistant) ) entry.add_to_hass(hass) - service_info = zeroconf.ZeroconfServiceInfo( + service_info = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="tubeszb-cc2652-poe.local.", @@ -397,7 +402,7 @@ async def test_legacy_zeroconf_discovery_confirm_final_abort_if_entries( hass: HomeAssistant, ) -> None: """Test discovery aborts if ZHA was set up after the confirmation dialog is shown.""" - service_info = zeroconf.ZeroconfServiceInfo( + service_info = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="tube._tube_zb_gw._tcp.local.", @@ -429,7 +434,7 @@ async def test_legacy_zeroconf_discovery_confirm_final_abort_if_entries( @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) async def test_discovery_via_usb(hass: HomeAssistant) -> None: """Test usb flow -- radio detected.""" - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -475,7 +480,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: @patch(f"zigpy_zigate.{PROBE_FUNCTION_PATH}", return_value=True) async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None: """Test zigate usb flow -- radio detected.""" - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="0403", vid="6015", @@ -528,7 +533,7 @@ async def test_zigate_discovery_via_usb(probe_mock, hass: HomeAssistant) -> None ) async def test_discovery_via_usb_no_radio(hass: HomeAssistant) -> None: """Test usb flow -- no radio detected.""" - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/null", pid="AAAA", vid="AAAA", @@ -561,7 +566,7 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} ).add_to_hass(hass) - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -595,7 +600,7 @@ async def test_discovery_via_usb_path_does_not_change(hass: HomeAssistant) -> No ) entry.add_to_hass(hass) - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -622,7 +627,7 @@ async def test_discovery_via_usb_deconz_already_discovered(hass: HomeAssistant) """Test usb flow -- deconz discovered.""" result = await hass.config_entries.flow.async_init( "deconz", - data=ssdp.SsdpServiceInfo( + data=SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", ssdp_location="http://1.2.3.4:80/", @@ -634,7 +639,7 @@ async def test_discovery_via_usb_deconz_already_discovered(hass: HomeAssistant) context={"source": SOURCE_SSDP}, ) await hass.async_block_till_done() - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -656,7 +661,7 @@ async def test_discovery_via_usb_deconz_already_setup(hass: HomeAssistant) -> No """Test usb flow -- deconz setup.""" MockConfigEntry(domain="deconz", data={}).add_to_hass(hass) await hass.async_block_till_done() - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -680,7 +685,7 @@ async def test_discovery_via_usb_deconz_ignored(hass: HomeAssistant) -> None: domain="deconz", source=config_entries.SOURCE_IGNORE, data={} ).add_to_hass(hass) await hass.async_block_till_done() - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -708,7 +713,7 @@ async def test_discovery_via_usb_zha_ignored_updates(hass: HomeAssistant) -> Non ) entry.add_to_hass(hass) await hass.async_block_till_done() - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -732,7 +737,7 @@ async def test_discovery_via_usb_zha_ignored_updates(hass: HomeAssistant) -> Non @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) async def test_legacy_zeroconf_discovery_already_setup(hass: HomeAssistant) -> None: """Test zeroconf flow -- radio detected.""" - service_info = zeroconf.ZeroconfServiceInfo( + service_info = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.200"), ip_addresses=[ip_address("192.168.1.200")], hostname="_tube_zb_gw._tcp.local.", @@ -1196,7 +1201,7 @@ async def test_onboarding_auto_formation_new_hardware( """Test auto network formation with new hardware during onboarding.""" mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) mock_app.get_device = MagicMock(return_value=MagicMock(spec=zigpy.device.Device)) - discovery_info = usb.UsbServiceInfo( + discovery_info = UsbServiceInfo( device="/dev/ttyZIGBEE", pid="AAAA", vid="AAAA", @@ -1909,9 +1914,18 @@ async def test_options_flow_migration_reset_old_adapter( assert result4["step_id"] == "choose_serial_port" -async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "device", + [ + "/dev/ttyAMA1", # CM4 + "/dev/ttyAMA10", # CM5, erroneously detected by pyserial + ], +) +async def test_config_flow_port_yellow_port_name( + hass: HomeAssistant, device: str +) -> None: """Test config flow serial port name for Yellow Zigbee radio.""" - port = com_port(device="/dev/ttyAMA1") + port = com_port(device=device) port.serial_number = None port.manufacturer = None port.description = None diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index ae96de44f17..8a587966f81 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -18,7 +18,7 @@ from homeassistant.components.zha.helpers import ( ) from homeassistant.const import STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE diff --git a/tests/components/zha/test_entity.py b/tests/components/zha/test_entity.py new file mode 100644 index 00000000000..add98bb96bf --- /dev/null +++ b/tests/components/zha/test_entity.py @@ -0,0 +1,47 @@ +"""Test ZHA entities.""" + +from zigpy.profiles import zha +from zigpy.zcl.clusters import general + +from homeassistant.components.zha.helpers import get_zha_gateway +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE + + +async def test_device_registry_via_device( + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, + device_registry: dr.DeviceRegistry, +) -> None: + """Test ZHA `via_device` is set correctly.""" + + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ) + + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + reg_coordinator_device = device_registry.async_get_device( + identifiers={("zha", str(gateway.state.node_info.ieee))} + ) + + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) + + assert reg_device.via_device_id == reg_coordinator_device.id diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index f8a809df51e..f52b403869e 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -9,7 +9,7 @@ from zigpy.application import ControllerApplication from zigpy.types.basic import uint16_t from zigpy.zcl.clusters import lighting -import homeassistant.components.zha.const as zha_const +from homeassistant.components.zha import const as zha_const from homeassistant.components.zha.helpers import ( cluster_command_schema_to_vol_schema, convert_to_zcl_values, @@ -18,7 +18,7 @@ from homeassistant.components.zha.helpers import ( get_zha_data, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_homeassistant_hardware.py b/tests/components/zha/test_homeassistant_hardware.py new file mode 100644 index 00000000000..72285521182 --- /dev/null +++ b/tests/components/zha/test_homeassistant_hardware.py @@ -0,0 +1,120 @@ +"""Test Home Assistant Hardware platform for ZHA.""" + +from unittest.mock import MagicMock, patch + +import pytest +from zigpy.application import ControllerApplication + +from homeassistant.components.homeassistant_hardware.helpers import ( + async_register_firmware_info_callback, +) +from homeassistant.components.homeassistant_hardware.util import ( + ApplicationType, + FirmwareInfo, + OwningIntegration, +) +from homeassistant.components.zha.homeassistant_hardware import get_firmware_info +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_get_firmware_info_normal(hass: HomeAssistant) -> None: + """Test `get_firmware_info`.""" + + zha = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={ + "device": { + "path": "/dev/ttyUSB1", + "baudrate": 115200, + "flow_control": None, + }, + "radio_type": "ezsp", + }, + version=4, + ) + zha.add_to_hass(hass) + zha.mock_state(hass, ConfigEntryState.LOADED) + + # With ZHA running + with patch( + "homeassistant.components.zha.homeassistant_hardware.get_zha_gateway" + ) as mock_get_zha_gateway: + mock_get_zha_gateway.return_value.state.node_info.version = "1.2.3.4" + fw_info_running = get_firmware_info(hass, zha) + + assert fw_info_running == FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.EZSP, + firmware_version="1.2.3.4", + source="zha", + owners=[OwningIntegration(config_entry_id=zha.entry_id)], + ) + assert await fw_info_running.is_running(hass) is True + + # With ZHA not running + zha.mock_state(hass, ConfigEntryState.NOT_LOADED) + fw_info_not_running = get_firmware_info(hass, zha) + + assert fw_info_not_running == FirmwareInfo( + device="/dev/ttyUSB1", + firmware_type=ApplicationType.EZSP, + firmware_version=None, + source="zha", + owners=[OwningIntegration(config_entry_id=zha.entry_id)], + ) + assert await fw_info_not_running.is_running(hass) is False + + +@pytest.mark.parametrize( + "data", + [ + # Missing data + {}, + # Bad radio type + {"device": {"path": "/dev/ttyUSB1"}, "radio_type": "znp"}, + ], +) +async def test_get_firmware_info_errors( + hass: HomeAssistant, data: dict[str, str | int | None] +) -> None: + """Test `get_firmware_info` with config entry data format errors.""" + zha = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data=data, + version=4, + ) + zha.add_to_hass(hass) + + assert (get_firmware_info(hass, zha)) is None + + +async def test_hardware_firmware_info_provider_notification( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_zigpy_connect: ControllerApplication, +) -> None: + """Test that the ZHA gateway provides hardware and firmware information.""" + config_entry.add_to_hass(hass) + + await async_setup_component(hass, "homeassistant_hardware", {}) + + callback = MagicMock() + async_register_firmware_info_callback(hass, "/dev/ttyUSB0", callback) + + await hass.config_entries.async_setup(config_entry.entry_id) + + callback.assert_called_once_with( + FirmwareInfo( + device="/dev/ttyUSB0", + firmware_type=ApplicationType.EZSP, + firmware_version="7.1.4.0 build 389", + source="zha", + owners=[OwningIntegration(config_entry_id=config_entry.entry_id)], + ) + ) diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 0a51aaa6dba..59494dd0d09 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -11,12 +11,12 @@ import zigpy.config from zigpy.config import CONF_DEVICE_PATH import zigpy.types -from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.const import DOMAIN from homeassistant.components.zha.radio_manager import ProbeResult, ZhaRadioManager from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.usb import UsbServiceInfo from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index c2925161748..af81ac0d586 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -1,17 +1,14 @@ """Test ZHA repairs.""" -from collections.abc import Callable from http import HTTPStatus -import logging from unittest.mock import Mock, call, patch import pytest -from universal_silabs_flasher.const import ApplicationType -from universal_silabs_flasher.flasher import Flasher from zigpy.application import ControllerApplication import zigpy.backups from zigpy.exceptions import NetworkSettingsInconsistent +from homeassistant.components.homeassistant_hardware.util import ApplicationType from homeassistant.components.homeassistant_sky_connect.const import ( # pylint: disable=hass-component-root-import DOMAIN as SKYCONNECT_DOMAIN, ) @@ -25,7 +22,6 @@ from homeassistant.components.zha.repairs.wrong_silabs_firmware import ( ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, HardwareType, _detect_radio_hardware, - probe_silabs_firmware_type, warn_on_wrong_silabs_firmware, ) from homeassistant.config_entries import ConfigEntryState @@ -41,15 +37,6 @@ SKYCONNECT_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8 CONNECT_ZBT1_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0" -def set_flasher_app_type(app_type: ApplicationType) -> Callable[[Flasher], None]: - """Set the app type on the flasher.""" - - def replacement(self: Flasher) -> None: - self.app_type = app_type - - return replacement - - def test_detect_radio_hardware(hass: HomeAssistant) -> None: """Test logic to detect radio hardware.""" skyconnect_config_entry = MockConfigEntry( @@ -143,9 +130,8 @@ async def test_multipan_firmware_repair( # ZHA fails to set up with ( patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=set_flasher_app_type(ApplicationType.CPC), - autospec=True, + "homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type", + return_value=ApplicationType.CPC, ), patch( "homeassistant.components.zha.Gateway.async_initialize", @@ -194,9 +180,8 @@ async def test_multipan_firmware_no_repair_on_probe_failure( # ZHA fails to set up with ( patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=set_flasher_app_type(None), - autospec=True, + "homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type", + return_value=None, ), patch( "homeassistant.components.zha.Gateway.async_initialize", @@ -231,9 +216,8 @@ async def test_multipan_firmware_retry_on_probe_ezsp( # ZHA fails to set up with ( patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=set_flasher_app_type(ApplicationType.EZSP), - autospec=True, + "homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type", + return_value=ApplicationType.EZSP, ), patch( "homeassistant.components.zha.Gateway.async_initialize", @@ -260,37 +244,12 @@ async def test_no_warn_on_socket(hass: HomeAssistant) -> None: """Test that no warning is issued when the device is a socket.""" with patch( "homeassistant.components.zha.repairs.wrong_silabs_firmware.probe_silabs_firmware_type", - autospec=True, ) as mock_probe: await warn_on_wrong_silabs_firmware(hass, device="socket://1.2.3.4:5678") mock_probe.assert_not_called() -async def test_probe_failure_exception_handling( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test that probe failures are handled gracefully.""" - logger = logging.getLogger( - "homeassistant.components.zha.repairs.wrong_silabs_firmware" - ) - orig_level = logger.level - - with ( - caplog.at_level(logging.DEBUG), - patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=RuntimeError(), - ) as mock_probe_app_type, - ): - logger.setLevel(logging.DEBUG) - await probe_silabs_firmware_type("/dev/ttyZigbee") - logger.setLevel(orig_level) - - mock_probe_app_type.assert_awaited() - assert "Failed to probe application type" in caplog.text - - async def test_inconsistent_settings_keep_new( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index f9837a7d016..5849cc6f233 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -28,7 +28,7 @@ from homeassistant.components.zha.helpers import ( ) from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from .common import find_entity_id diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index 19b9733e4f5..880e5c889ec 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -23,7 +23,7 @@ from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 37b1dde7316..bcdc0c3ce16 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -133,9 +133,9 @@ def climate_radio_thermostat_ct100_plus_state_fixture() -> dict[str, Any]: name="climate_radio_thermostat_ct100_plus_different_endpoints_state", scope="package", ) -def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture() -> ( - dict[str, Any] -): +def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture() -> dict[ + str, Any +]: """Load the thermostat fixture state with values on different endpoints. This device is a radio thermostat ct100. @@ -336,9 +336,9 @@ def lock_id_lock_as_id150_state_fixture() -> dict[str, Any]: @pytest.fixture( name="climate_radio_thermostat_ct101_multiple_temp_units_state", scope="package" ) -def climate_radio_thermostat_ct101_multiple_temp_units_state_fixture() -> ( - dict[str, Any] -): +def climate_radio_thermostat_ct101_multiple_temp_units_state_fixture() -> dict[ + str, Any +]: """Load the climate multiple temp units node state fixture data.""" return load_json_object_fixture( "climate_radio_thermostat_ct101_multiple_temp_units_state.json", DOMAIN diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index a3f70e92dcf..6f341f8f77b 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -4930,6 +4930,9 @@ async def test_subscribe_node_statistics( assert msg["error"]["code"] == ERR_NOT_LOADED +@pytest.mark.skip( + reason="The test needs to be updated to reflect what happens when resetting the controller" +) async def test_hard_reset_controller( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index b60515cacd4..e7239c23de6 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -16,13 +16,13 @@ from serial.tools.list_ports_common import ListPortInfo from zwave_js_server.version import VersionInfo from homeassistant import config_entries -from homeassistant.components import usb -from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import ADDON_SLUG, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo +from homeassistant.helpers.service_info.usb import UsbServiceInfo +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -33,7 +33,7 @@ ADDON_DISCOVERY_INFO = { } -USB_DISCOVERY_INFO = usb.UsbServiceInfo( +USB_DISCOVERY_INFO = UsbServiceInfo( device="/dev/zwave", pid="AAAA", vid="AAAA", @@ -42,7 +42,7 @@ USB_DISCOVERY_INFO = usb.UsbServiceInfo( manufacturer="test", ) -NORTEK_ZIGBEE_DISCOVERY_INFO = usb.UsbServiceInfo( +NORTEK_ZIGBEE_DISCOVERY_INFO = UsbServiceInfo( device="/dev/zigbee", pid="8A2A", vid="10C4", @@ -51,7 +51,7 @@ NORTEK_ZIGBEE_DISCOVERY_INFO = usb.UsbServiceInfo( manufacturer="nortek", ) -CP2652_ZIGBEE_DISCOVERY_INFO = usb.UsbServiceInfo( +CP2652_ZIGBEE_DISCOVERY_INFO = UsbServiceInfo( device="/dev/zigbee", pid="EA60", vid="10C4", diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index d237a6e410a..a46320168eb 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -10,8 +10,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.core import HomeAssistant -import homeassistant.helpers.device_registry as dr -import homeassistant.helpers.issue_registry as ir +from homeassistant.helpers import device_registry as dr, issue_registry as ir from tests.components.repairs import ( async_process_repairs_platforms, diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index ddfd205b017..d26cccbc7d5 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -10,7 +10,7 @@ from homeassistant.components.zwave_js.helpers import ZwaveValueMatcher from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers import entity_registry as er from .common import replace_value_of_zwave_value diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index a71df8751b6..f784d7db2db 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -4,14 +4,14 @@ from ipaddress import ip_address from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components import zeroconf from homeassistant.components.zwave_me.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry -MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( +MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.14"), ip_addresses=[ip_address("192.168.1.14")], hostname="mock_hostname", diff --git a/tests/conftest.py b/tests/conftest.py index 2cefe72f414..7d9fa7eda2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch from aiohttp import client +from aiohttp.resolver import AsyncResolver from aiohttp.test_utils import ( BaseTestServer, TestClient, @@ -32,6 +33,7 @@ import bcrypt import freezegun import multidict import pytest +import pytest_asyncio import pytest_socket import requests_mock import respx @@ -42,7 +44,7 @@ from homeassistant import block_async_io from homeassistant.exceptions import ServiceNotFound # Setup patching of recorder functions before any other Home Assistant imports -from . import patch_recorder # noqa: F401, isort:skip +from . import patch_recorder # Setup patching of dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip @@ -88,7 +90,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.translation import _TranslationsCacheData from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util, location +from homeassistant.util import dt as dt_util, location as location_util from homeassistant.util.async_ import create_eager_task, get_scheduled_timer_handles from homeassistant.util.json import json_loads @@ -100,6 +102,7 @@ from .typing import ( MqttMockHAClient, MqttMockHAClientGenerator, MqttMockPahoClient, + RecorderInstanceContextManager, RecorderInstanceGenerator, WebSocketGenerator, ) @@ -115,6 +118,7 @@ from .common import ( # noqa: E402, isort:skip CLIENT_ID, INSTANCES, MockConfigEntry, + MockMqttReasonCode, MockUser, async_fire_mqtt_message, async_test_home_assistant, @@ -248,7 +252,9 @@ def check_real[**_P, _R](func: Callable[_P, Coroutine[Any, Any, _R]]): # Guard a few functions that would make network connections -location.async_detect_location_info = check_real(location.async_detect_location_info) +location_util.async_detect_location_info = check_real( + location_util.async_detect_location_info +) @pytest.fixture(name="caplog") @@ -411,7 +417,9 @@ def verify_cleanup( try: # Verify respx.mock has been cleaned up - assert not respx.mock.routes, "respx.mock routes not cleaned up, maybe the test needs to be decorated with @respx.mock" + assert not respx.mock.routes, ( + "respx.mock routes not cleaned up, maybe the test needs to be decorated with @respx.mock" + ) finally: # Clear mock routes not break subsequent tests respx.mock.clear() @@ -579,7 +587,7 @@ async def hass( exceptions.append( Exception( "Received exception handler without exception, " - f"but with message: {context["message"]}" + f"but with message: {context['message']}" ) ) orig_exception_handler(loop, context) @@ -923,7 +931,13 @@ def fail_on_log_exception( @pytest.fixture def mqtt_config_entry_data() -> dict[str, Any] | None: - """Fixture to allow overriding MQTT config.""" + """Fixture to allow overriding MQTT entry data.""" + return None + + +@pytest.fixture +def mqtt_config_entry_options() -> dict[str, Any] | None: + """Fixture to allow overriding MQTT entry options.""" return None @@ -956,17 +970,23 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]: def _async_fire_mqtt_message(topic, payload, qos, retain): async_fire_mqtt_message(hass, topic, payload or b"", qos, retain) mid = get_mid() - hass.loop.call_soon(mock_client.on_publish, 0, 0, mid) + hass.loop.call_soon( + mock_client.on_publish, Mock(), 0, mid, MockMqttReasonCode(), None + ) return FakeInfo(mid) def _subscribe(topic, qos=0): mid = get_mid() - hass.loop.call_soon(mock_client.on_subscribe, 0, 0, mid) + hass.loop.call_soon( + mock_client.on_subscribe, Mock(), 0, mid, [MockMqttReasonCode()], None + ) return (0, mid) def _unsubscribe(topic): mid = get_mid() - hass.loop.call_soon(mock_client.on_unsubscribe, 0, 0, mid) + hass.loop.call_soon( + mock_client.on_unsubscribe, Mock(), 0, mid, [MockMqttReasonCode()], None + ) return (0, mid) def _connect(*args, **kwargs): @@ -975,7 +995,7 @@ def mqtt_client_mock(hass: HomeAssistant) -> Generator[MqttMockPahoClient]: # the behavior. mock_client.reconnect() hass.loop.call_soon_threadsafe( - mock_client.on_connect, mock_client, None, 0, 0, 0 + mock_client.on_connect, mock_client, None, 0, MockMqttReasonCode() ) mock_client.on_socket_open( mock_client, None, Mock(fileno=Mock(return_value=-1)) @@ -1000,6 +1020,7 @@ async def mqtt_mock( mock_hass_config: None, mqtt_client_mock: MqttMockPahoClient, mqtt_config_entry_data: dict[str, Any] | None, + mqtt_config_entry_options: dict[str, Any] | None, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> AsyncGenerator[MqttMockHAClient]: """Fixture to mock MQTT component.""" @@ -1011,6 +1032,7 @@ async def _mqtt_mock_entry( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, mqtt_config_entry_data: dict[str, Any] | None, + mqtt_config_entry_options: dict[str, Any] | None, ) -> AsyncGenerator[MqttMockHAClientGenerator]: """Fixture to mock a delayed setup of the MQTT config entry.""" # Local import to avoid processing MQTT modules when running a testcase @@ -1018,17 +1040,19 @@ async def _mqtt_mock_entry( from homeassistant.components import mqtt # pylint: disable=import-outside-toplevel if mqtt_config_entry_data is None: - mqtt_config_entry_data = { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - } + mqtt_config_entry_data = {mqtt.CONF_BROKER: "mock-broker"} + if mqtt_config_entry_options is None: + mqtt_config_entry_options = {mqtt.CONF_BIRTH_MESSAGE: {}} await hass.async_block_till_done() entry = MockConfigEntry( data=mqtt_config_entry_data, + options=mqtt_config_entry_options, domain=mqtt.DOMAIN, title="MQTT", + version=1, + minor_version=2, ) entry.add_to_hass(hass) @@ -1044,12 +1068,11 @@ async def _mqtt_mock_entry( # Assert that MQTT is setup assert real_mqtt_instance is not None, "MQTT was not setup correctly" - mock_mqtt_instance.conf = real_mqtt_instance.conf # For diagnostics mock_mqtt_instance._mqttc = mqtt_client_mock # connected set to True to get a more realistic behavior when subscribing mock_mqtt_instance.connected = True - mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, 0, 0) + mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, MockMqttReasonCode()) async_dispatcher_send(hass, mqtt.MQTT_CONNECTION_STATE, True) await hass.async_block_till_done() @@ -1139,6 +1162,7 @@ async def mqtt_mock_entry( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, mqtt_config_entry_data: dict[str, Any] | None, + mqtt_config_entry_options: dict[str, Any] | None, ) -> AsyncGenerator[MqttMockHAClientGenerator]: """Set up an MQTT config entry.""" @@ -1155,7 +1179,7 @@ async def mqtt_mock_entry( return await mqtt_mock_entry(_async_setup_config_entry) async with _mqtt_mock_entry( - hass, mqtt_client_mock, mqtt_config_entry_data + hass, mqtt_client_mock, mqtt_config_entry_data, mqtt_config_entry_options ) as mqtt_mock_entry: yield _setup_mqtt_entry @@ -1163,15 +1187,31 @@ async def mqtt_mock_entry( @pytest.fixture(autouse=True, scope="session") def mock_network() -> Generator[None]: """Mock network.""" - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=[ - Mock( - nice_name="eth0", - ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)], - index=0, - ) - ], + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[ + Mock( + nice_name="eth0", + ips=[Mock(is_IPv6=False, ip="10.10.10.10", network_prefix=24)], + index=0, + ) + ], + ), + patch( + "homeassistant.components.network.async_get_loaded_adapters", + return_value=[ + { + "auto": True, + "default": True, + "enabled": True, + "index": 0, + "ipv4": [{"address": "10.10.10.10", "network_prefix": 24}], + "ipv6": [], + "name": "eth0", + } + ], + ), ): yield @@ -1220,6 +1260,30 @@ def disable_translations_once( translations_once.start() +@pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session") +async def mock_zeroconf_resolver() -> AsyncGenerator[_patch]: + """Mock out the zeroconf resolver.""" + patcher = patch( + "homeassistant.helpers.aiohttp_client._async_make_resolver", + return_value=AsyncResolver(), + ) + patcher.start() + try: + yield patcher + finally: + patcher.stop() + + +@pytest.fixture +def disable_mock_zeroconf_resolver( + mock_zeroconf_resolver: _patch, +) -> Generator[None]: + """Disable the zeroconf resolver.""" + mock_zeroconf_resolver.stop() + yield + mock_zeroconf_resolver.start() + + @pytest.fixture def mock_zeroconf() -> Generator[MagicMock]: """Mock zeroconf.""" @@ -1496,7 +1560,7 @@ async def async_test_recorder( enable_migrate_event_type_ids: bool, enable_migrate_entity_ids: bool, enable_migrate_event_ids: bool, -) -> AsyncGenerator[RecorderInstanceGenerator]: +) -> AsyncGenerator[RecorderInstanceContextManager]: """Yield context manager to setup recorder instance.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components import recorder @@ -1662,7 +1726,7 @@ async def async_test_recorder( @pytest.fixture async def async_setup_recorder_instance( - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, ) -> AsyncGenerator[RecorderInstanceGenerator]: """Yield callable to setup recorder instance.""" @@ -1675,7 +1739,7 @@ async def async_setup_recorder_instance( expected_setup_result: bool = True, wait_recorder: bool = True, wait_recorder_setup: bool = True, - ) -> AsyncGenerator[recorder.Recorder]: + ) -> recorder.Recorder: """Set up and return recorder instance.""" return await stack.enter_async_context( @@ -1694,7 +1758,7 @@ async def async_setup_recorder_instance( @pytest.fixture async def recorder_mock( recorder_config: dict[str, Any] | None, - async_test_recorder: RecorderInstanceGenerator, + async_test_recorder: RecorderInstanceContextManager, hass: HomeAssistant, ) -> AsyncGenerator[recorder.Recorder]: """Fixture with in-memory recorder.""" diff --git a/tests/helpers/snapshots/test_entity_platform.ambr b/tests/helpers/snapshots/test_entity_platform.ambr index 84cbb07bd73..55ff772e08e 100644 --- a/tests/helpers/snapshots/test_entity_platform.ambr +++ b/tests/helpers/snapshots/test_entity_platform.ambr @@ -3,6 +3,7 @@ DeviceRegistryEntrySnapshot({ 'area_id': 'heliport', 'config_entries': , + 'config_entries_subentries': , 'configuration_url': 'http://192.168.0.100/config', 'connections': set({ tuple( @@ -35,3 +36,40 @@ 'via_device_id': , }) # --- +# name: test_device_info_called.1 + DeviceRegistryEntrySnapshot({ + 'area_id': 'heliport', + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'http://192.168.0.100/config', + 'connections': set({ + tuple( + 'mac', + 'efgh', + ), + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': 'test-hw', + 'id': , + 'identifiers': set({ + tuple( + 'hue', + 'efgh', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'test-manuf', + 'model': 'test-model', + 'model_id': None, + 'name': 'test-name', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': 'Heliport', + 'sw_version': 'test-sw', + 'via_device_id': , + }) +# --- diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 1788da74c3b..13cb25bc516 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -21,7 +21,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.aiohttp_client as client +from homeassistant.helpers import aiohttp_client as client from homeassistant.util.color import RGBColor from homeassistant.util.ssl import SSLCipherList @@ -390,3 +390,16 @@ async def test_client_session_immutable_headers(hass: HomeAssistant) -> None: with pytest.raises(AttributeError): session.headers.update({"user-agent": "bla"}) + + +@pytest.mark.usefixtures("disable_mock_zeroconf_resolver") +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_async_mdnsresolver( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test async_mdnsresolver.""" + resp = aioclient_mock.post("http://localhost/xyz", json={"x": 1}) + session = client.async_create_clientsession(hass) + resp = await session.post("http://localhost/xyz", json={"x": 1}) + assert resp.status == 200 + assert await resp.json() == {"x": 1} diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 74f55c86a6c..c69f039027e 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -7,6 +7,13 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory import pytest +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import ( area_registry as ar, @@ -18,6 +25,27 @@ from homeassistant.util.dt import utcnow from tests.common import ANY, async_capture_events, flush_store +@pytest.fixture +async def mock_temperature_humidity_entity(hass: HomeAssistant) -> None: + """Mock temperature and humidity sensors.""" + hass.states.async_set( + "sensor.mock_temperature", + "20", + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, + ) + hass.states.async_set( + "sensor.mock_humidity", + "50", + { + ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + ) + + async def test_list_areas(area_registry: ar.AreaRegistry) -> None: """Make sure that we can read areas.""" area_registry.async_create("mock") @@ -31,6 +59,7 @@ async def test_create_area( hass: HomeAssistant, freezer: FrozenDateTimeFactory, area_registry: ar.AreaRegistry, + mock_temperature_humidity_entity: None, ) -> None: """Make sure that we can create an area.""" update_events = async_capture_events(hass, ar.EVENT_AREA_REGISTRY_UPDATED) @@ -48,6 +77,8 @@ async def test_create_area( picture=None, created_at=utcnow(), modified_at=utcnow(), + temperature_entity_id=None, + humidity_entity_id=None, ) assert len(area_registry.areas) == 1 @@ -67,6 +98,8 @@ async def test_create_area( aliases={"alias_1", "alias_2"}, labels={"label1", "label2"}, picture="/image/example.png", + temperature_entity_id="sensor.mock_temperature", + humidity_entity_id="sensor.mock_humidity", ) assert area2 == ar.AreaEntry( @@ -79,6 +112,8 @@ async def test_create_area( picture="/image/example.png", created_at=utcnow(), modified_at=utcnow(), + temperature_entity_id="sensor.mock_temperature", + humidity_entity_id="sensor.mock_humidity", ) assert len(area_registry.areas) == 2 assert area.created_at != area2.created_at @@ -164,6 +199,7 @@ async def test_update_area( floor_registry: fr.FloorRegistry, label_registry: lr.LabelRegistry, freezer: FrozenDateTimeFactory, + mock_temperature_humidity_entity: None, ) -> None: """Make sure that we can read areas.""" created_at = datetime.fromisoformat("2024-01-01T01:00:00+00:00") @@ -184,6 +220,8 @@ async def test_update_area( labels={"label1", "label2"}, name="mock1", picture="/image/example.png", + temperature_entity_id="sensor.mock_temperature", + humidity_entity_id="sensor.mock_humidity", ) assert updated_area != area @@ -197,6 +235,8 @@ async def test_update_area( picture="/image/example.png", created_at=created_at, modified_at=modified_at, + temperature_entity_id="sensor.mock_temperature", + humidity_entity_id="sensor.mock_humidity", ) assert len(area_registry.areas) == 1 @@ -274,6 +314,55 @@ async def test_update_area_with_normalized_name_already_in_use( assert len(area_registry.areas) == 2 +@pytest.mark.parametrize( + ("create_kwargs", "error_message"), + [ + ( + {"temperature_entity_id": "sensor.invalid"}, + "Entity sensor.invalid does not exist", + ), + ( + {"temperature_entity_id": "light.kitchen"}, + "Entity light.kitchen is not a temperature sensor", + ), + ( + {"temperature_entity_id": "sensor.random"}, + "Entity sensor.random is not a temperature sensor", + ), + ( + {"humidity_entity_id": "sensor.invalid"}, + "Entity sensor.invalid does not exist", + ), + ( + {"humidity_entity_id": "light.kitchen"}, + "Entity light.kitchen is not a humidity sensor", + ), + ( + {"humidity_entity_id": "sensor.random"}, + "Entity sensor.random is not a humidity sensor", + ), + ], +) +async def test_update_area_entity_validation( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + mock_temperature_humidity_entity: None, + create_kwargs: dict[str, Any], + error_message: str, +) -> None: + """Make sure that we can't update an area with an invalid entity.""" + area = area_registry.async_create("mock") + hass.states.async_set("light.kitchen", "on", {}) + hass.states.async_set("sensor.random", "3", {}) + + with pytest.raises(ValueError) as e_info: + area_registry.async_update(area.id, **create_kwargs) + assert str(e_info.value) == error_message + + assert area.temperature_entity_id is None + assert area.humidity_entity_id is None + + async def test_load_area(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None: """Make sure that we can load/save data correctly.""" area1 = area_registry.async_create("mock1") @@ -298,6 +387,8 @@ async def test_loading_area_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test loading stored areas on start.""" + created_at = datetime.fromisoformat("2024-01-01T01:00:00+00:00") + modified_at = datetime.fromisoformat("2024-02-01T01:00:00+00:00") hass_storage[ar.STORAGE_KEY] = { "version": ar.STORAGE_VERSION_MAJOR, "minor_version": ar.STORAGE_VERSION_MINOR, @@ -311,8 +402,10 @@ async def test_loading_area_from_storage( "labels": ["mock-label1", "mock-label2"], "name": "mock", "picture": "blah", - "created_at": utcnow().isoformat(), - "modified_at": utcnow().isoformat(), + "created_at": created_at.isoformat(), + "modified_at": modified_at.isoformat(), + "temperature_entity_id": "sensor.mock_temperature", + "humidity_entity_id": "sensor.mock_humidity", } ] }, @@ -322,6 +415,20 @@ async def test_loading_area_from_storage( registry = ar.async_get(hass) assert len(registry.areas) == 1 + area = registry.areas["12345A"] + assert area == ar.AreaEntry( + aliases={"alias_1", "alias_2"}, + floor_id="first_floor", + icon="mdi:garage", + id="12345A", + labels={"mock-label1", "mock-label2"}, + name="mock", + picture="blah", + created_at=created_at, + modified_at=modified_at, + temperature_entity_id="sensor.mock_temperature", + humidity_entity_id="sensor.mock_humidity", + ) @pytest.mark.parametrize("load_registries", [False]) @@ -359,6 +466,8 @@ async def test_migration_from_1_1( "picture": None, "created_at": "1970-01-01T00:00:00+00:00", "modified_at": "1970-01-01T00:00:00+00:00", + "temperature_entity_id": None, + "humidity_entity_id": None, } ] }, diff --git a/tests/helpers/test_chat_session.py b/tests/helpers/test_chat_session.py new file mode 100644 index 00000000000..f6c2fe5057d --- /dev/null +++ b/tests/helpers/test_chat_session.py @@ -0,0 +1,96 @@ +"""Test the chat session helper.""" + +from collections.abc import Generator +from datetime import timedelta +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import chat_session +from homeassistant.util import dt as dt_util, ulid as ulid_util + +from tests.common import async_fire_time_changed + + +@pytest.fixture +def mock_ulid() -> Generator[Mock]: + """Mock the ulid library.""" + with patch("homeassistant.helpers.chat_session.ulid_now") as mock_ulid_now: + mock_ulid_now.return_value = "mock-ulid" + yield mock_ulid_now + + +@pytest.mark.parametrize( + ("start_id", "given_id"), + [ + (None, "mock-ulid"), + # This ULID is not known as a session + ("01JHXE0952TSJCFJZ869AW6HMD", "mock-ulid"), + ("not-a-ulid", "not-a-ulid"), + ], +) +async def test_conversation_id( + hass: HomeAssistant, + start_id: str | None, + given_id: str, + mock_ulid: Mock, +) -> None: + """Test conversation ID generation.""" + with chat_session.async_get_chat_session(hass, start_id) as session: + assert session.conversation_id == given_id + + +async def test_context_var(hass: HomeAssistant) -> None: + """Test context var.""" + with chat_session.async_get_chat_session(hass) as session: + with chat_session.async_get_chat_session( + hass, session.conversation_id + ) as session2: + assert session is session2 + + with chat_session.async_get_chat_session(hass, None) as session2: + assert session.conversation_id != session2.conversation_id + + with chat_session.async_get_chat_session(hass, "something else") as session2: + assert session.conversation_id != session2.conversation_id + + with chat_session.async_get_chat_session( + hass, ulid_util.ulid_now() + ) as session2: + assert session.conversation_id != session2.conversation_id + + +async def test_cleanup( + hass: HomeAssistant, +) -> None: + """Test cleanup of the chat session.""" + with chat_session.async_get_chat_session(hass) as session: + conversation_id = session.conversation_id + + # Reuse conversation ID to ensure we can chat with same session + with chat_session.async_get_chat_session(hass, conversation_id) as session: + assert session.conversation_id == conversation_id + + # Set the last updated to be older than the timeout + hass.data[chat_session.DATA_CHAT_SESSION][conversation_id].last_updated = ( + dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT + ) + + async_fire_time_changed( + hass, + dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT + timedelta(seconds=1), + ) + + # Should not be cleaned up, but it should have scheduled another cleanup + with chat_session.async_get_chat_session(hass, conversation_id) as session: + assert session.conversation_id == conversation_id + + async_fire_time_changed( + hass, + dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT * 2 + timedelta(seconds=1), + ) + + # It should be cleaned up now and we start a new conversation + with chat_session.async_get_chat_session(hass, conversation_id) as session: + assert session.conversation_id != conversation_id diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index de7edf42dc2..fc2df8552e7 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -9,12 +9,12 @@ import voluptuous as vol from homeassistant.config import YAML_CONFIG_FILE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.check_config import ( CheckConfigError, HomeAssistantConfig, async_check_ha_config_file, ) -import homeassistant.helpers.config_validation as cv from homeassistant.requirements import RequirementsNotFound from tests.common import ( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 1ec78b20535..b8c8c8a18c8 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -30,7 +30,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.typing import WebSocketGenerator diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 13e28bb8840..172aa393538 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,6 +1,8 @@ """Tests for the Config Entry Flow helper.""" -from collections.abc import Generator +import asyncio +from collections.abc import Callable, Generator +from contextlib import contextmanager from unittest.mock import Mock, PropertyMock, patch import pytest @@ -13,22 +15,44 @@ from homeassistant.helpers import config_entry_flow from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform +@contextmanager +def _make_discovery_flow_conf( + has_discovered_devices: Callable[[], asyncio.Future[bool] | bool], +) -> Generator[None]: + with patch.dict(config_entries.HANDLERS): + config_entry_flow.register_discovery_flow( + "test", "Test", has_discovered_devices + ) + yield + + @pytest.fixture -def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]: - """Register a handler.""" +def async_discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]: + """Register a handler with an async discovery function.""" handler_conf = {"discovered": False} async def has_discovered_devices(hass: HomeAssistant) -> bool: """Mock if we have discovered devices.""" return handler_conf["discovered"] - with patch.dict(config_entries.HANDLERS): - config_entry_flow.register_discovery_flow( - "test", "Test", has_discovered_devices - ) + with _make_discovery_flow_conf(has_discovered_devices): yield handler_conf +@pytest.fixture +def discovery_flow_conf(hass: HomeAssistant) -> Generator[dict[str, bool]]: + """Register a handler with a async friendly callback function.""" + handler_conf = {"discovered": False} + + def has_discovered_devices(hass: HomeAssistant) -> bool: + """Mock if we have discovered devices.""" + return handler_conf["discovered"] + + with _make_discovery_flow_conf(has_discovered_devices): + yield handler_conf + handler_conf = {"discovered": False} + + @pytest.fixture def webhook_flow_conf(hass: HomeAssistant) -> Generator[None]: """Register a handler.""" @@ -95,6 +119,33 @@ async def test_user_has_confirmation( assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY +async def test_user_has_confirmation_async_discovery_flow( + hass: HomeAssistant, async_discovery_flow_conf: dict[str, bool] +) -> None: + """Test user requires confirmation to setup with an async has_discovered_devices.""" + async_discovery_flow_conf["discovered"] = True + mock_platform(hass, "test.config_flow", None) + + result = await hass.config_entries.flow.async_init( + "test", context={"source": config_entries.SOURCE_USER}, data={} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm" + + progress = hass.config_entries.flow.async_progress() + assert len(progress) == 1 + assert progress[0]["flow_id"] == result["flow_id"] + assert progress[0]["context"] == { + "confirm_only": True, + "source": config_entries.SOURCE_USER, + "unique_id": "test", + } + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + @pytest.mark.parametrize( "source", [ diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 52def52f3f0..0fc6b582bb5 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -595,6 +595,13 @@ async def test_abort_discovered_existing_entries( assert result["reason"] == "already_configured" +@pytest.mark.parametrize( + ("additional_components", "expected_redirect_uri"), + [ + ([], "https://example.com/auth/external/callback"), + (["my"], "https://my.home-assistant.io/redirect/oauth"), + ], +) @pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( hass: HomeAssistant, @@ -602,8 +609,12 @@ async def test_full_flow( local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, + additional_components: list[str], + expected_redirect_uri: str, ) -> None: """Check full flow.""" + for component in additional_components: + assert await setup.async_setup_component(hass, component, {}) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() @@ -625,14 +636,14 @@ async def test_full_flow( hass, { "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", + "redirect_uri": expected_redirect_uri, }, ) assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" + f"&redirect_uri={expected_redirect_uri}" f"&state={state}&scope=read+write" ) diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index 6fa758aec6e..b2dd8943e78 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -4,6 +4,7 @@ import asyncio from datetime import timedelta import logging from unittest.mock import AsyncMock, Mock +import weakref import pytest @@ -529,3 +530,37 @@ async def test_background( async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) await hass.async_block_till_done(wait_background_tasks=False) assert len(calls) == 2 + + +async def test_shutdown_releases_parent_class(hass: HomeAssistant) -> None: + """Test shutdown releases parent class. + + See https://github.com/home-assistant/core/issues/137237 + """ + calls = [] + + class SomeClass: + def run_func(self) -> None: + calls.append(None) + + my_class = SomeClass() + my_class_weak_ref = weakref.ref(my_class) + + debouncer = debounce.Debouncer( + hass, + _LOGGER, + cooldown=0.01, + immediate=True, + function=my_class.run_func, + ) + + # Debouncer keeps a reference to the function, prevening GC + del my_class + await debouncer.async_call() + await hass.async_block_till_done() + assert len(calls) == 1 + assert my_class_weak_ref() is not None + + # Debouncer shutdown releases the class + debouncer.async_shutdown() + assert my_class_weak_ref() is None diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 4cf7e851af3..a74055c59ec 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -295,7 +295,7 @@ def _get_value( return obj.value if isinstance(obj, DeprecatedConstantEnum): - return obj.enum.value + return obj.enum if isinstance(obj, DeprecatedAlias): return obj.value diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index cf7bbe7d1e2..29edfb3fea7 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -173,6 +173,109 @@ async def test_multiple_config_entries( assert entry3.primary_config_entry == config_entry_1.entry_id +async def test_multiple_config_subentries( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Make sure we do not get duplicate entries.""" + config_entry_1 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_2.add_to_hass(hass) + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.config_entries == {config_entry_1.entry_id} + assert entry.config_entries_subentries == {config_entry_1.entry_id: {None}} + entry_id = entry.id + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id=None, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.id == entry_id + assert entry.config_entries == {config_entry_1.entry_id} + assert entry.config_entries_subentries == {config_entry_1.entry_id: {None}} + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.id == entry_id + assert entry.config_entries == {config_entry_1.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1"} + } + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-2", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.id == entry_id + assert entry.config_entries == {config_entry_1.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"} + } + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_2.entry_id, + config_subentry_id="mock-subentry-id-2-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.id == entry_id + assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + + @pytest.mark.parametrize("load_registries", [False]) @pytest.mark.usefixtures("freezer") async def test_loading_from_storage( @@ -191,6 +294,7 @@ async def test_loading_from_storage( { "area_id": "12345A", "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": "https://example.com/config", "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": created_at, @@ -215,6 +319,7 @@ async def test_loading_from_storage( "deleted_devices": [ { "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "connections": [["Zigbee", "23.45.67.89.01"]], "created_at": created_at, "id": "bcdefghijklmn", @@ -233,6 +338,7 @@ async def test_loading_from_storage( assert registry.deleted_devices["bcdefghijklmn"] == dr.DeletedDeviceEntry( config_entries={mock_config_entry.entry_id}, + config_entries_subentries={mock_config_entry.entry_id: {None}}, connections={("Zigbee", "23.45.67.89.01")}, created_at=datetime.fromisoformat(created_at), id="bcdefghijklmn", @@ -251,6 +357,7 @@ async def test_loading_from_storage( assert entry == dr.DeviceEntry( area_id="12345A", config_entries={mock_config_entry.entry_id}, + config_entries_subentries={mock_config_entry.entry_id: {None}}, configuration_url="https://example.com/config", connections={("Zigbee", "01.23.45.67.89")}, created_at=datetime.fromisoformat(created_at), @@ -285,6 +392,7 @@ async def test_loading_from_storage( ) assert entry == dr.DeviceEntry( config_entries={mock_config_entry.entry_id}, + config_entries_subentries={mock_config_entry.entry_id: {None}}, connections={("Zigbee", "23.45.67.89.01")}, created_at=datetime.fromisoformat(created_at), id="bcdefghijklmn", @@ -384,6 +492,7 @@ async def test_migration_from_1_1( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -407,6 +516,7 @@ async def test_migration_from_1_1( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -431,6 +541,7 @@ async def test_migration_from_1_1( "deleted_devices": [ { "config_entries": ["123456"], + "config_entries_subentries": {"123456": [None]}, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", "id": "deletedid", @@ -528,6 +639,7 @@ async def test_migration_from_1_2( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -551,6 +663,7 @@ async def test_migration_from_1_2( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -662,6 +775,7 @@ async def test_migration_fom_1_3( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -685,6 +799,7 @@ async def test_migration_fom_1_3( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -798,6 +913,7 @@ async def test_migration_from_1_4( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -821,6 +937,7 @@ async def test_migration_from_1_4( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -936,6 +1053,7 @@ async def test_migration_from_1_5( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -959,6 +1077,7 @@ async def test_migration_from_1_5( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -1076,6 +1195,7 @@ async def test_migration_from_1_6( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -1099,6 +1219,7 @@ async def test_migration_from_1_6( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -1218,6 +1339,7 @@ async def test_migration_from_1_7( { "area_id": None, "config_entries": [mock_config_entry.entry_id], + "config_entries_subentries": {mock_config_entry.entry_id: [None]}, "configuration_url": None, "connections": [["Zigbee", "01.23.45.67.89"]], "created_at": "1970-01-01T00:00:00+00:00", @@ -1241,6 +1363,7 @@ async def test_migration_from_1_7( { "area_id": None, "config_entries": ["234567"], + "config_entries_subentries": {"234567": [None]}, "configuration_url": None, "connections": [], "created_at": "1970-01-01T00:00:00+00:00", @@ -1303,6 +1426,10 @@ async def test_removing_config_entries( assert entry.id == entry2.id assert entry.id != entry3.id assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.config_entries_subentries == { + config_entry_1.entry_id: {None}, + config_entry_2.entry_id: {None}, + } device_registry.async_clear_config_entry(config_entry_1.entry_id) entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) @@ -1311,6 +1438,7 @@ async def test_removing_config_entries( ) assert entry.config_entries == {config_entry_2.entry_id} + assert entry.config_entries_subentries == {config_entry_2.entry_id: {None}} assert entry3_removed is None await hass.async_block_till_done() @@ -1325,6 +1453,7 @@ async def test_removing_config_entries( "device_id": entry.id, "changes": { "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": {config_entry_1.entry_id: {None}}, }, } assert update_events[2].data == { @@ -1336,6 +1465,10 @@ async def test_removing_config_entries( "device_id": entry.id, "changes": { "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: {None}, + config_entry_2.entry_id: {None}, + }, "primary_config_entry": config_entry_1.entry_id, }, } @@ -1382,6 +1515,10 @@ async def test_deleted_device_removing_config_entries( assert entry.id == entry2.id assert entry.id != entry3.id assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry2.config_entries_subentries == { + config_entry_1.entry_id: {None}, + config_entry_2.entry_id: {None}, + } device_registry.async_remove_device(entry.id) device_registry.async_remove_device(entry3.id) @@ -1400,6 +1537,7 @@ async def test_deleted_device_removing_config_entries( "device_id": entry2.id, "changes": { "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": {config_entry_1.entry_id: {None}}, }, } assert update_events[2].data == { @@ -1418,10 +1556,16 @@ async def test_deleted_device_removing_config_entries( device_registry.async_clear_config_entry(config_entry_1.entry_id) assert len(device_registry.devices) == 0 assert len(device_registry.deleted_devices) == 2 + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == {config_entry_2.entry_id} + assert entry.config_entries_subentries == {config_entry_2.entry_id: {None}} device_registry.async_clear_config_entry(config_entry_2.entry_id) assert len(device_registry.devices) == 0 assert len(device_registry.deleted_devices) == 2 + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == set() + assert entry.config_entries_subentries == {} # No event when a deleted device is purged await hass.async_block_till_done() @@ -1454,6 +1598,427 @@ async def test_deleted_device_removing_config_entries( assert entry3.id != entry4.id +async def test_removing_config_subentries( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Make sure we do not get duplicate entries.""" + update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) + config_entry_1 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_2.add_to_hass(hass) + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry2 = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-2", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry4 = device_registry.async_get_or_create( + config_entry_id=config_entry_2.entry_id, + config_subentry_id="mock-subentry-id-2-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(device_registry.devices) == 1 + assert entry.id == entry2.id + assert entry.id == entry3.id + assert entry.id == entry4.id + assert entry4.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry4.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + + device_registry.async_update_device( + entry.id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id=None, + ) + entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) + assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + + hass.config_entries.async_remove_subentry(config_entry_1, "mock-subentry-id-1-1") + entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) + assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + + hass.config_entries.async_remove_subentry(config_entry_1, "mock-subentry-id-1-2") + entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")}) + assert entry.config_entries == {config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_2.entry_id: {"mock-subentry-id-2-1"} + } + + hass.config_entries.async_remove_subentry(config_entry_2, "mock-subentry-id-2-1") + assert device_registry.async_get_device(identifiers={("bridgeid", "0123")}) is None + assert device_registry.async_get_device(identifiers={("bridgeid", "4567")}) is None + + await hass.async_block_till_done() + + assert len(update_events) == 8 + assert update_events[0].data == { + "action": "create", + "device_id": entry.id, + } + assert update_events[1].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries_subentries": {config_entry_1.entry_id: {None}}, + }, + } + assert update_events[2].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries_subentries": { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1"} + }, + }, + } + assert update_events[3].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: { + None, + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + } + }, + "identifiers": {("bridgeid", "0123")}, + }, + } + assert update_events[4].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries_subentries": { + config_entry_1.entry_id: { + None, + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + }, + config_entry_2.entry_id: { + "mock-subentry-id-2-1", + }, + }, + }, + } + assert update_events[5].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries_subentries": { + config_entry_1.entry_id: { + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + }, + config_entry_2.entry_id: { + "mock-subentry-id-2-1", + }, + }, + }, + } + assert update_events[6].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: { + "mock-subentry-id-1-2", + }, + config_entry_2.entry_id: { + "mock-subentry-id-2-1", + }, + }, + "primary_config_entry": config_entry_1.entry_id, + }, + } + assert update_events[7].data == { + "action": "remove", + "device_id": entry.id, + } + + +async def test_deleted_device_removing_config_subentries( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Make sure we do not get duplicate entries.""" + update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) + config_entry_1 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_2.add_to_hass(hass) + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry2 = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-2", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry4 = device_registry.async_get_or_create( + config_entry_id=config_entry_2.entry_id, + config_subentry_id="mock-subentry-id-2-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(device_registry.devices) == 1 + assert len(device_registry.deleted_devices) == 0 + assert entry.id == entry2.id + assert entry.id == entry3.id + assert entry.id == entry4.id + assert entry4.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry4.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + + device_registry.async_remove_device(entry.id) + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 1 + + await hass.async_block_till_done() + + assert len(update_events) == 5 + assert update_events[0].data == { + "action": "create", + "device_id": entry.id, + } + assert update_events[1].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries_subentries": {config_entry_1.entry_id: {None}}, + }, + } + assert update_events[2].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries_subentries": { + config_entry_1.entry_id: {None, "mock-subentry-id-1-1"} + }, + }, + } + assert update_events[3].data == { + "action": "update", + "device_id": entry.id, + "changes": { + "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: { + None, + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + } + }, + "identifiers": {("bridgeid", "0123")}, + }, + } + assert update_events[4].data == { + "action": "remove", + "device_id": entry.id, + } + + device_registry.async_clear_config_subentry(config_entry_1.entry_id, None) + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + assert entry.orphaned_timestamp is None + + hass.config_entries.async_remove_subentry(config_entry_1, "mock-subentry-id-1-1") + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + assert entry.orphaned_timestamp is None + + # Remove the same subentry again + device_registry.async_clear_config_subentry( + config_entry_1.entry_id, "mock-subentry-id-1-1" + ) + assert ( + device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) is entry + ) + + hass.config_entries.async_remove_subentry(config_entry_1, "mock-subentry-id-1-2") + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == {config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_2.entry_id: {"mock-subentry-id-2-1"} + } + assert entry.orphaned_timestamp is None + + hass.config_entries.async_remove_subentry(config_entry_2, "mock-subentry-id-2-1") + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == set() + assert entry.config_entries_subentries == {} + assert entry.orphaned_timestamp is not None + + # No event when a deleted device is purged + await hass.async_block_till_done() + assert len(update_events) == 5 + + # Re-add, expect to keep the device id + hass.config_entries.async_add_subentry( + config_entry_2, + config_entries.ConfigSubentry( + data={}, + subentry_id="mock-subentry-id-2-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + restored_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_2.entry_id, + config_subentry_id="mock-subentry-id-2-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert restored_entry.id == entry.id + + # Remove again, and trigger purge + device_registry.async_remove_device(entry.id) + hass.config_entries.async_remove_subentry(config_entry_2, "mock-subentry-id-2-1") + entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) + assert entry.config_entries == set() + assert entry.config_entries_subentries == {} + assert entry.orphaned_timestamp is not None + + future_time = time.time() + dr.ORPHANED_DEVICE_KEEP_SECONDS + 1 + + with patch("time.time", return_value=future_time): + device_registry.async_purge_expired_orphaned_devices() + + assert len(device_registry.devices) == 0 + assert len(device_registry.deleted_devices) == 0 + + # Re-add, expect to get a new device id after the purge + new_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert new_entry.id != entry.id + + async def test_removing_area_id( device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry ) -> None: @@ -1834,6 +2399,7 @@ async def test_update( assert updated_entry == dr.DeviceEntry( area_id="12345A", config_entries={mock_config_entry.entry_id}, + config_entries_subentries={mock_config_entry.entry_id: {None}}, configuration_url="https://example.com/config", connections={("mac", "65:43:21:fe:dc:ba")}, created_at=created_at, @@ -2090,6 +2656,7 @@ async def test_update_remove_config_entries( "device_id": entry2.id, "changes": { "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": {config_entry_1.entry_id: {None}}, }, } assert update_events[2].data == { @@ -2100,7 +2667,11 @@ async def test_update_remove_config_entries( "action": "update", "device_id": entry.id, "changes": { - "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id} + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: {None}, + config_entry_2.entry_id: {None}, + }, }, } assert update_events[4].data == { @@ -2112,6 +2683,11 @@ async def test_update_remove_config_entries( config_entry_2.entry_id, config_entry_3.entry_id, }, + "config_entries_subentries": { + config_entry_1.entry_id: {None}, + config_entry_2.entry_id: {None}, + config_entry_3.entry_id: {None}, + }, "primary_config_entry": config_entry_1.entry_id, }, } @@ -2119,7 +2695,11 @@ async def test_update_remove_config_entries( "action": "update", "device_id": entry2.id, "changes": { - "config_entries": {config_entry_2.entry_id, config_entry_3.entry_id} + "config_entries": {config_entry_2.entry_id, config_entry_3.entry_id}, + "config_entries_subentries": { + config_entry_2.entry_id: {None}, + config_entry_3.entry_id: {None}, + }, }, } assert update_events[6].data == { @@ -2128,6 +2708,282 @@ async def test_update_remove_config_entries( } +async def test_update_remove_config_subentries( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Make sure we do not get duplicate entries.""" + update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) + config_entry_1 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_1.add_to_hass(hass) + config_entry_2 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ) + ) + config_entry_2.add_to_hass(hass) + config_entry_3 = MockConfigEntry() + config_entry_3.add_to_hass(hass) + + entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry_id = entry.id + assert entry.config_entries == {config_entry_1.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-1"} + } + + entry = device_registry.async_update_device( + entry_id, + add_config_entry_id=config_entry_1.entry_id, + add_config_subentry_id="mock-subentry-id-1-2", + ) + assert entry.config_entries == {config_entry_1.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"} + } + + # Try adding the same subentry again + assert ( + device_registry.async_update_device( + entry_id, + add_config_entry_id=config_entry_1.entry_id, + add_config_subentry_id="mock-subentry-id-1-2", + ) + is entry + ) + + entry = device_registry.async_update_device( + entry_id, + add_config_entry_id=config_entry_2.entry_id, + add_config_subentry_id="mock-subentry-id-2-1", + ) + assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id} + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + } + + entry = device_registry.async_update_device( + entry_id, + add_config_entry_id=config_entry_3.entry_id, + add_config_subentry_id=None, + ) + assert entry.config_entries == { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + } + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + config_entry_3.entry_id: {None}, + } + + # Try to add a subentry without specifying entry + with pytest.raises( + HomeAssistantError, + match="Can't add config subentry without specifying config entry", + ): + device_registry.async_update_device(entry_id, add_config_subentry_id="blabla") + + # Try to add an unknown subentry + with pytest.raises( + HomeAssistantError, + match=f"Config entry {config_entry_3.entry_id} has no subentry blabla", + ): + device_registry.async_update_device( + entry_id, + add_config_entry_id=config_entry_3.entry_id, + add_config_subentry_id="blabla", + ) + + # Try to remove a subentry without specifying entry + with pytest.raises( + HomeAssistantError, + match="Can't remove config subentry without specifying config entry", + ): + device_registry.async_update_device( + entry_id, remove_config_subentry_id="blabla" + ) + + assert len(device_registry.devices) == 1 + + entry = device_registry.async_update_device( + entry_id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id="mock-subentry-id-1-1", + ) + assert entry.config_entries == { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + } + assert entry.config_entries_subentries == { + config_entry_1.entry_id: {"mock-subentry-id-1-2"}, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + config_entry_3.entry_id: {None}, + } + + # Try removing the same subentry again + assert ( + device_registry.async_update_device( + entry_id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id="mock-subentry-id-1-1", + ) + is entry + ) + + entry = device_registry.async_update_device( + entry_id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id="mock-subentry-id-1-2", + ) + assert entry.config_entries == {config_entry_2.entry_id, config_entry_3.entry_id} + assert entry.config_entries_subentries == { + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + config_entry_3.entry_id: {None}, + } + + entry = device_registry.async_update_device( + entry_id, + remove_config_entry_id=config_entry_2.entry_id, + remove_config_subentry_id="mock-subentry-id-2-1", + ) + assert entry.config_entries == {config_entry_3.entry_id} + assert entry.config_entries_subentries == { + config_entry_3.entry_id: {None}, + } + + entry = device_registry.async_update_device( + entry_id, + remove_config_entry_id=config_entry_3.entry_id, + remove_config_subentry_id=None, + ) + assert entry is None + + await hass.async_block_till_done() + + assert len(update_events) == 8 + assert update_events[0].data == { + "action": "create", + "device_id": entry_id, + } + assert update_events[1].data == { + "action": "update", + "device_id": entry_id, + "changes": { + "config_entries_subentries": { + config_entry_1.entry_id: {"mock-subentry-id-1-1"} + }, + }, + } + assert update_events[2].data == { + "action": "update", + "device_id": entry_id, + "changes": { + "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: { + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + } + }, + }, + } + assert update_events[3].data == { + "action": "update", + "device_id": entry_id, + "changes": { + "config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}, + "config_entries_subentries": { + config_entry_1.entry_id: { + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + }, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + }, + }, + } + assert update_events[4].data == { + "action": "update", + "device_id": entry_id, + "changes": { + "config_entries_subentries": { + config_entry_1.entry_id: { + "mock-subentry-id-1-1", + "mock-subentry-id-1-2", + }, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + config_entry_3.entry_id: {None}, + }, + }, + } + assert update_events[5].data == { + "action": "update", + "device_id": entry_id, + "changes": { + "config_entries": { + config_entry_1.entry_id, + config_entry_2.entry_id, + config_entry_3.entry_id, + }, + "config_entries_subentries": { + config_entry_1.entry_id: { + "mock-subentry-id-1-2", + }, + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + config_entry_3.entry_id: {None}, + }, + "primary_config_entry": config_entry_1.entry_id, + }, + } + assert update_events[6].data == { + "action": "update", + "device_id": entry_id, + "changes": { + "config_entries": {config_entry_2.entry_id, config_entry_3.entry_id}, + "config_entries_subentries": { + config_entry_2.entry_id: {"mock-subentry-id-2-1"}, + config_entry_3.entry_id: {None}, + }, + }, + } + assert update_events[7].data == { + "action": "remove", + "device_id": entry_id, + } + + async def test_update_suggested_area( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -2276,7 +3132,9 @@ async def test_cleanup_startup(hass: HomeAssistant) -> None: @pytest.mark.parametrize("load_registries", [False]) -async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: +async def test_cleanup_entity_registry_change( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test we run a cleanup when entity registry changes. Don't pre-load the registries as the debouncer will then not be waiting for @@ -2284,8 +3142,14 @@ async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: """ await dr.async_load(hass) await er.async_load(hass) + dev_reg = dr.async_get(hass) ent_reg = er.async_get(hass) + entry = dev_reg.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + with patch( "homeassistant.helpers.device_registry.Debouncer.async_schedule_call" ) as mock_call: @@ -2299,7 +3163,7 @@ async def test_cleanup_entity_registry_change(hass: HomeAssistant) -> None: assert len(mock_call.mock_calls) == 0 # Device ID update triggers - ent_reg.async_get_or_create("light", "hue", "e1", device_id="bla") + ent_reg.async_get_or_create("light", "hue", "e1", device_id=entry.id) await hass.async_block_till_done() assert len(mock_call.mock_calls) == 1 @@ -2534,6 +3398,7 @@ async def test_restore_shared_device( "device_id": entry.id, "changes": { "config_entries": {config_entry_1.entry_id}, + "config_entries_subentries": {config_entry_1.entry_id: {None}}, "identifiers": {("entry_123", "0123")}, }, } @@ -2558,6 +3423,7 @@ async def test_restore_shared_device( "device_id": entry.id, "changes": { "config_entries": {config_entry_2.entry_id}, + "config_entries_subentries": {config_entry_2.entry_id: {None}}, "identifiers": {("entry_234", "2345")}, }, } @@ -2863,6 +3729,7 @@ async def test_loading_invalid_configuration_url_from_storage( { "area_id": None, "config_entries": ["1234"], + "config_entries_subentries": {"1234": [None]}, "configuration_url": "invalid", "connections": [], "created_at": "2024-01-01T00:00:00+00:00", @@ -3370,6 +4237,39 @@ async def test_device_registry_identifiers_collision( assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers) +async def test_device_registry_deleted_device_collision( + hass: HomeAssistant, device_registry: dr.DeviceRegistry +) -> None: + """Test update collisions with deleted devices in the device registry.""" + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + device1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE")}, + manufacturer="manufacturer", + model="model", + ) + assert len(device_registry.deleted_devices) == 0 + + device_registry.async_remove_device(device1.id) + assert len(device_registry.deleted_devices) == 1 + + device2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert len(device_registry.deleted_devices) == 1 + + device_registry.async_update_device( + device2.id, + merge_connections={(dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE")}, + ) + assert len(device_registry.deleted_devices) == 0 + + async def test_primary_config_entry( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dc579ab6e8d..6cf0e7c54d2 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -4,13 +4,14 @@ import asyncio from collections.abc import Iterable import dataclasses from datetime import timedelta +from enum import IntFlag import logging import threading from typing import Any from unittest.mock import MagicMock, PropertyMock, patch from freezegun.api import FrozenDateTimeFactory -from propcache import cached_property +from propcache.api import cached_property import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.core import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import UNDEFINED, UndefinedType from tests.common import ( @@ -985,7 +986,7 @@ async def _test_friendly_name( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities([ent]) @@ -1313,7 +1314,7 @@ async def test_entity_name_translation_placeholder_errors( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities([ent]) @@ -1541,7 +1542,7 @@ async def test_friendly_name_updated( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -2485,6 +2486,31 @@ async def test_cached_entity_property_override(hass: HomeAssistant) -> None: return "🤡" +async def test_entity_report_deprecated_supported_features_values( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test reporting deprecated supported feature values only happens once.""" + ent = entity.Entity() + + class MockEntityFeatures(IntFlag): + VALUE1 = 1 + VALUE2 = 2 + + ent._report_deprecated_supported_features_values(MockEntityFeatures(2)) + assert ( + "is using deprecated supported features values which will be removed" + in caplog.text + ) + assert "MockEntityFeatures.VALUE2" in caplog.text + + caplog.clear() + ent._report_deprecated_supported_features_values(MockEntityFeatures(2)) + assert ( + "is using deprecated supported features values which will be removed" + not in caplog.text + ) + + async def test_remove_entity_registry( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 940bd3e37fd..20c243d0701 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -28,7 +28,7 @@ from homeassistant.helpers.entity_component import EntityComponent, async_update from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index e80006dff84..41b7271150a 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -11,7 +11,7 @@ import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigSubentryData from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, PERCENTAGE, EntityCategory from homeassistant.core import ( CoreState, @@ -36,9 +36,12 @@ from homeassistant.helpers.entity_component import ( DEFAULT_SCAN_INTERVAL, EntityComponent, ) -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, @@ -223,7 +226,7 @@ async def test_set_scan_interval_via_platform(hass: HomeAssistant) -> None: def platform_setup( hass: HomeAssistant, config: ConfigType, - add_entities: entity_platform.AddEntitiesCallback, + add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Test the platform setup.""" @@ -862,13 +865,29 @@ async def test_setup_entry( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" - async_add_entities([MockEntity(name="test1", unique_id="unique")]) + async_add_entities([MockEntity(name="test1", unique_id="unique1")]) + async_add_entities( + [MockEntity(name="test2", unique_id="unique2")], + config_subentry_id="mock-subentry-id-1", + ) platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry = MockConfigEntry( + entry_id="super-mock-id", + subentries_data=( + ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ), + ) + config_entry.add_to_hass(hass) entity_platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -877,11 +896,16 @@ async def test_setup_entry( await hass.async_block_till_done() full_name = f"{config_entry.domain}.{entity_platform.domain}" assert full_name in hass.config.components - assert len(hass.states.async_entity_ids()) == 1 - assert len(entity_registry.entities) == 1 + assert len(hass.states.async_entity_ids()) == 2 + assert len(entity_registry.entities) == 2 entity_registry_entry = entity_registry.entities["test_domain.test1"] assert entity_registry_entry.config_entry_id == "super-mock-id" + assert entity_registry_entry.config_subentry_id is None + + entity_registry_entry = entity_registry.entities["test_domain.test2"] + assert entity_registry_entry.config_entry_id == "super-mock-id" + assert entity_registry_entry.config_subentry_id == "mock-subentry-id-1" async def test_setup_entry_platform_not_ready( @@ -1137,7 +1161,18 @@ async def test_device_info_called( snapshot: SnapshotAssertion, ) -> None: """Test device info is forwarded correctly.""" - config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry = MockConfigEntry( + entry_id="super-mock-id", + subentries_data=( + ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ), + ) config_entry.add_to_hass(hass) via = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -1150,7 +1185,7 @@ async def test_device_info_called( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -1176,6 +1211,28 @@ async def test_device_info_called( ), ] ) + async_add_entities( + [ + # Valid device info + MockEntity( + unique_id="efgh", + device_info={ + "identifiers": {("hue", "efgh")}, + "configuration_url": "http://192.168.0.100/config", + "connections": {(dr.CONNECTION_NETWORK_MAC, "efgh")}, + "manufacturer": "test-manuf", + "model": "test-model", + "name": "test-name", + "sw_version": "test-sw", + "hw_version": "test-hw", + "suggested_area": "Heliport", + "entry_type": dr.DeviceEntryType.SERVICE, + "via_device": ("hue", "via-id"), + }, + ), + ], + config_subentry_id="mock-subentry-id-1", + ) platform = MockPlatform(async_setup_entry=async_setup_entry) entity_platform = MockEntityPlatform( @@ -1185,11 +1242,20 @@ async def test_device_info_called( assert await entity_platform.async_setup_entry(config_entry) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == 2 + assert len(hass.states.async_entity_ids()) == 3 device = device_registry.async_get_device(identifiers={("hue", "1234")}) assert device == snapshot assert device.config_entries == {config_entry.entry_id} + assert device.config_entries_subentries == {config_entry.entry_id: {None}} + assert device.primary_config_entry == config_entry.entry_id + assert device.via_device_id == via.id + device = device_registry.async_get_device(identifiers={("hue", "efgh")}) + assert device == snapshot + assert device.config_entries == {config_entry.entry_id} + assert device.config_entries_subentries == { + config_entry.entry_id: {"mock-subentry-id-1"} + } assert device.primary_config_entry == config_entry.entry_id assert device.via_device_id == via.id @@ -1213,7 +1279,7 @@ async def test_device_info_not_overrides( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -1266,7 +1332,7 @@ async def test_device_info_homeassistant_url( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -1318,7 +1384,7 @@ async def test_device_info_change_to_no_url( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -1390,7 +1456,7 @@ async def test_entity_disabled_by_device( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities([entity_disabled]) @@ -1876,7 +1942,7 @@ async def test_setup_entry_with_entities_that_block_forever( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -1886,6 +1952,7 @@ async def test_setup_entry_with_entities_that_block_forever( platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -1924,7 +1991,7 @@ async def test_cancellation_is_not_blocked( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -1934,6 +2001,7 @@ async def test_cancellation_is_not_blocked( platform = MockPlatform(async_setup_entry=async_setup_entry) config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) platform = MockEntityPlatform( hass, platform_name=config_entry.domain, platform=platform ) @@ -2021,7 +2089,7 @@ async def test_entity_name_influences_entity_id( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -2109,7 +2177,7 @@ async def test_translated_entity_name_influences_entity_id( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities( @@ -2197,7 +2265,7 @@ async def test_translated_device_class_name_influences_entity_id( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities([TranslatedDeviceClassEntity(device_class, has_entity_name)]) @@ -2259,7 +2327,7 @@ async def test_device_name_defaulting_config_entry( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities([DeviceNameEntity()]) @@ -2315,7 +2383,7 @@ async def test_device_type_error_checking( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setup entry method.""" async_add_entities([DeviceNameEntity()]) @@ -2334,3 +2402,41 @@ async def test_device_type_error_checking( assert len(device_registry.devices) == 0 assert len(entity_registry.entities) == number_of_entities assert len(hass.states.async_all()) == number_of_entities + + +async def test_add_entity_unknown_subentry( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test adding an entity to an unknown subentry.""" + + async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, + ) -> None: + """Mock setup entry method.""" + async_add_entities( + [MockEntity(name="test", unique_id="unique")], + config_subentry_id="unknown-subentry", + ) + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + config_entry.add_to_hass(hass) + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert not await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + full_name = f"{config_entry.domain}.{entity_platform.domain}" + assert full_name not in hass.config.components + assert len(hass.states.async_entity_ids()) == 0 + assert len(entity_registry.entities) == 0 + + assert ( + "Can't add entities to unknown subentry unknown-subentry " + "of config entry super-mock-id" + ) in caplog.text diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 97f7e1dcc56..416f2d5121d 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -72,11 +72,30 @@ def test_get_or_create_suggested_object_id(entity_registry: er.EntityRegistry) - def test_get_or_create_updates_data( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: """Test that we update data in get_or_create.""" - orig_config_entry = MockConfigEntry(domain="light") + config_subentry_id = "blabla" + orig_config_entry = MockConfigEntry( + domain="light", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id=config_subentry_id, + subentry_type="test", + title="Mock title", + unique_id="test", + ) + ], + ) + orig_config_entry.add_to_hass(hass) + orig_device_entry = device_registry.async_get_or_create( + config_entry_id=orig_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) created = datetime.fromisoformat("2024-02-14T12:00:00.0+00:00") freezer.move_to(created) @@ -86,7 +105,8 @@ def test_get_or_create_updates_data( "5678", capabilities={"max": 100}, config_entry=orig_config_entry, - device_id="mock-dev-id", + config_subentry_id=config_subentry_id, + device_id=orig_device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, has_entity_name=True, @@ -99,7 +119,7 @@ def test_get_or_create_updates_data( unit_of_measurement="initial-unit_of_measurement", ) - assert set(entity_registry.async_device_ids()) == {"mock-dev-id"} + assert set(entity_registry.async_device_ids()) == {orig_device_entry.id} assert orig_entry == er.RegistryEntry( "light.hue_5678", @@ -107,9 +127,10 @@ def test_get_or_create_updates_data( "hue", capabilities={"max": 100}, config_entry_id=orig_config_entry.entry_id, + config_subentry_id=config_subentry_id, created_at=created, device_class=None, - device_id="mock-dev-id", + device_id=orig_device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, has_entity_name=True, @@ -127,6 +148,11 @@ def test_get_or_create_updates_data( ) new_config_entry = MockConfigEntry(domain="light") + new_config_entry.add_to_hass(hass) + new_device_entry = device_registry.async_get_or_create( + config_entry_id=new_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, + ) modified = created + timedelta(minutes=5) freezer.move_to(modified) @@ -136,7 +162,8 @@ def test_get_or_create_updates_data( "5678", capabilities={"new-max": 150}, config_entry=new_config_entry, - device_id="new-mock-dev-id", + config_subentry_id=None, + device_id=new_device_entry.id, disabled_by=er.RegistryEntryDisabler.USER, entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=False, @@ -157,9 +184,10 @@ def test_get_or_create_updates_data( area_id=None, capabilities={"new-max": 150}, config_entry_id=new_config_entry.entry_id, + config_subentry_id=None, created_at=created, device_class=None, - device_id="new-mock-dev-id", + device_id=new_device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated entity_category=EntityCategory.DIAGNOSTIC, has_entity_name=False, @@ -176,7 +204,7 @@ def test_get_or_create_updates_data( unit_of_measurement="updated-unit_of_measurement", ) - assert set(entity_registry.async_device_ids()) == {"new-mock-dev-id"} + assert set(entity_registry.async_device_ids()) == {new_device_entry.id} modified = created + timedelta(minutes=5) freezer.move_to(modified) @@ -262,10 +290,18 @@ def test_create_triggers_save(entity_registry: er.EntityRegistry) -> None: async def test_loading_saving_data( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test that we load/save data correctly.""" mock_config = MockConfigEntry(domain="light") + mock_config.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) orig_entry1 = entity_registry.async_get_or_create("light", "hue", "1234") orig_entry2 = entity_registry.async_get_or_create( @@ -274,7 +310,7 @@ async def test_loading_saving_data( "5678", capabilities={"max": 100}, config_entry=mock_config, - device_id="mock-dev-id", + device_id=device_entry.id, disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, hidden_by=er.RegistryEntryHider.INTEGRATION, @@ -338,7 +374,7 @@ async def test_loading_saving_data( assert new_entry2.capabilities == {"max": 100} assert new_entry2.config_entry_id == mock_config.entry_id assert new_entry2.device_class == "user-class" - assert new_entry2.device_id == "mock-dev-id" + assert new_entry2.device_id == device_entry.id assert new_entry2.disabled_by is er.RegistryEntryDisabler.HASS assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" @@ -476,6 +512,7 @@ async def test_load_bad_data( "capabilities": None, "categories": {}, "config_entry_id": None, + "config_subentry_id": None, "created_at": "2024-02-14T12:00:00.900075+00:00", "device_class": None, "device_id": None, @@ -506,6 +543,7 @@ async def test_load_bad_data( "capabilities": None, "categories": {}, "config_entry_id": None, + "config_subentry_id": None, "created_at": "2024-02-14T12:00:00.900075+00:00", "device_class": None, "device_id": None, @@ -534,6 +572,7 @@ async def test_load_bad_data( "deleted_entities": [ { "config_entry_id": None, + "config_subentry_id": None, "created_at": "2024-02-14T12:00:00.900075+00:00", "entity_id": "test.test3", "id": "00003", @@ -544,6 +583,7 @@ async def test_load_bad_data( }, { "config_entry_id": None, + "config_subentry_id": None, "created_at": "2024-02-14T12:00:00.900075+00:00", "entity_id": "test.test4", "id": "00004", @@ -596,11 +636,13 @@ async def test_updating_config_entry_id( """Test that we update config entry id in registry.""" update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) mock_config_1 = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config_1.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config_1 ) mock_config_2 = MockConfigEntry(domain="light", entry_id="mock-id-2") + mock_config_2.add_to_hass(hass) entry2 = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config_2 ) @@ -627,6 +669,7 @@ async def test_removing_config_entry_id( """Test that we update config entry id in registry.""" update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config @@ -650,11 +693,14 @@ async def test_removing_config_entry_id( async def test_deleted_entity_removing_config_entry_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry, ) -> None: """Test that we update config entry id in registry on deleted entity.""" mock_config1 = MockConfigEntry(domain="light", entry_id="mock-id-1") mock_config2 = MockConfigEntry(domain="light", entry_id="mock-id-2") + mock_config1.add_to_hass(hass) + mock_config2.add_to_hass(hass) entry1 = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config1 @@ -685,6 +731,118 @@ async def test_deleted_entity_removing_config_entry_id( assert entity_registry.deleted_entities[("light", "hue", "1234")] == deleted_entry2 +async def test_removing_config_subentry_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test that we update config subentry id in registry.""" + update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) + mock_config = MockConfigEntry( + domain="light", + entry_id="mock-id-1", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ) + ], + ) + mock_config.add_to_hass(hass) + + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=mock_config, + config_subentry_id="mock-subentry-id-1", + ) + assert entry.config_subentry_id == "mock-subentry-id-1" + hass.config_entries.async_remove_subentry(mock_config, "mock-subentry-id-1") + + assert not entity_registry.entities + + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0].data == { + "action": "create", + "entity_id": entry.entity_id, + } + assert update_events[1].data == { + "action": "remove", + "entity_id": entry.entity_id, + } + + +async def test_deleted_entity_removing_config_subentry_id( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test that we update config subentry id in registry on deleted entity.""" + mock_config = MockConfigEntry( + domain="light", + entry_id="mock-id-1", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + mock_config.add_to_hass(hass) + + entry1 = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=mock_config, + config_subentry_id="mock-subentry-id-1", + ) + assert entry1.config_subentry_id == "mock-subentry-id-1" + entry2 = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + config_entry=mock_config, + config_subentry_id="mock-subentry-id-2", + ) + assert entry2.config_subentry_id == "mock-subentry-id-2" + entity_registry.async_remove(entry1.entity_id) + entity_registry.async_remove(entry2.entity_id) + + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 2 + deleted_entry1 = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry1.config_entry_id == "mock-id-1" + assert deleted_entry1.config_subentry_id == "mock-subentry-id-1" + assert deleted_entry1.orphaned_timestamp is None + deleted_entry2 = entity_registry.deleted_entities[("light", "hue", "1234")] + assert deleted_entry2.config_entry_id == "mock-id-1" + assert deleted_entry2.config_subentry_id == "mock-subentry-id-2" + assert deleted_entry2.orphaned_timestamp is None + + hass.config_entries.async_remove_subentry(mock_config, "mock-subentry-id-1") + assert len(entity_registry.entities) == 0 + assert len(entity_registry.deleted_entities) == 2 + deleted_entry1 = entity_registry.deleted_entities[("light", "hue", "5678")] + assert deleted_entry1.config_entry_id is None + assert deleted_entry1.config_subentry_id is None + assert deleted_entry1.orphaned_timestamp is not None + assert entity_registry.deleted_entities[("light", "hue", "1234")] == deleted_entry2 + + async def test_removing_area_id(entity_registry: er.EntityRegistry) -> None: """Make sure we can clear area id.""" entry = entity_registry.async_get_or_create("light", "hue", "5678") @@ -740,6 +898,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) "capabilities": {}, "categories": {}, "config_entry_id": None, + "config_subentry_id": None, "created_at": "1970-01-01T00:00:00+00:00", "device_id": None, "disabled_by": None, @@ -918,6 +1077,7 @@ async def test_migration_1_11( "capabilities": {}, "categories": {}, "config_entry_id": None, + "config_subentry_id": None, "created_at": "1970-01-01T00:00:00+00:00", "device_id": None, "disabled_by": None, @@ -946,6 +1106,7 @@ async def test_migration_1_11( "deleted_entities": [ { "config_entry_id": None, + "config_subentry_id": None, "created_at": "1970-01-01T00:00:00+00:00", "entity_id": "test.deleted_entity", "id": "23456", @@ -959,9 +1120,12 @@ async def test_migration_1_11( } -async def test_update_entity_unique_id(entity_registry: er.EntityRegistry) -> None: +async def test_update_entity_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test entity's unique_id is updated.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config @@ -987,10 +1151,12 @@ async def test_update_entity_unique_id(entity_registry: er.EntityRegistry) -> No async def test_update_entity_unique_id_conflict( + hass: HomeAssistant, entity_registry: er.EntityRegistry, ) -> None: """Test migration raises when unique_id already in use.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config ) @@ -1079,9 +1245,12 @@ async def test_update_entity_entity_id_entity_id( assert entity_registry.async_get(state_entity_id) is None -async def test_update_entity(entity_registry: er.EntityRegistry) -> None: +async def test_update_entity( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test updating entity.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config ) @@ -1106,9 +1275,12 @@ async def test_update_entity(entity_registry: er.EntityRegistry) -> None: entry = updated_entry -async def test_update_entity_options(entity_registry: er.EntityRegistry) -> None: +async def test_update_entity_options( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test updating entity.""" mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + mock_config.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "5678", config_entry=mock_config ) @@ -1161,6 +1333,7 @@ async def test_disabled_by(entity_registry: er.EntityRegistry) -> None: async def test_disabled_by_config_entry_pref( + hass: HomeAssistant, entity_registry: er.EntityRegistry, ) -> None: """Test config entry preference setting disabled_by.""" @@ -1169,6 +1342,7 @@ async def test_disabled_by_config_entry_pref( entry_id="mock-id-1", pref_disable_new_entities=True, ) + mock_config.add_to_hass(hass) entry = entity_registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config ) @@ -1392,7 +1566,7 @@ async def test_remove_config_entry_from_device_removes_entities_2( config_entry_2.entry_id, } - # Create one entity for each config entry + # Create an entity without config entry entry_1 = entity_registry.async_get_or_create( "light", "hue", @@ -1412,6 +1586,208 @@ async def test_remove_config_entry_from_device_removes_entities_2( assert entity_registry.async_is_registered(entry_1.entity_id) +async def test_remove_config_subentry_from_device_removes_entities( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that we remove entities tied to a device when config subentry is removed.""" + config_entry_1 = MockConfigEntry( + domain="hue", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + config_entry_1.add_to_hass(hass) + + # Create device with three config subentries + device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-2", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert device_entry.config_entries == {config_entry_1.entry_id} + assert device_entry.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1", "mock-subentry-id-2"}, + } + + # Create one entity entry for each config entry or subentry + entry_1 = entity_registry.async_get_or_create( + "light", + "hue", + "1234", + config_entry=config_entry_1, + config_subentry_id="mock-subentry-id-1", + device_id=device_entry.id, + ) + + entry_2 = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry_1, + config_subentry_id="mock-subentry-id-2", + device_id=device_entry.id, + ) + + entry_3 = entity_registry.async_get_or_create( + "sensor", + "device_tracker", + "6789", + config_entry=config_entry_1, + config_subentry_id=None, + device_id=device_entry.id, + ) + + assert entity_registry.async_is_registered(entry_1.entity_id) + assert entity_registry.async_is_registered(entry_2.entity_id) + assert entity_registry.async_is_registered(entry_3.entity_id) + + # Remove the first config subentry from the device, the entity associated with it + # should be removed + device_registry.async_update_device( + device_entry.id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id="mock-subentry-id-1", + ) + await hass.async_block_till_done() + + assert device_registry.async_get(device_entry.id) + assert not entity_registry.async_is_registered(entry_1.entity_id) + assert entity_registry.async_is_registered(entry_2.entity_id) + assert entity_registry.async_is_registered(entry_3.entity_id) + + # Remove the second config subentry from the device, the entity associated with it + # should be removed + device_registry.async_update_device( + device_entry.id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id=None, + ) + await hass.async_block_till_done() + + assert device_registry.async_get(device_entry.id) + assert not entity_registry.async_is_registered(entry_1.entity_id) + assert entity_registry.async_is_registered(entry_2.entity_id) + assert not entity_registry.async_is_registered(entry_3.entity_id) + + # Remove the third config subentry from the device, the entity associated with it + # (and the device itself) should be removed + device_registry.async_update_device( + device_entry.id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id="mock-subentry-id-2", + ) + await hass.async_block_till_done() + + assert not device_registry.async_get(device_entry.id) + assert not entity_registry.async_is_registered(entry_1.entity_id) + assert not entity_registry.async_is_registered(entry_2.entity_id) + assert not entity_registry.async_is_registered(entry_3.entity_id) + + +async def test_remove_config_subentry_from_device_removes_entities_2( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that we don't remove entities with no config entry when device is modified.""" + config_entry_1 = MockConfigEntry( + domain="hue", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + config_entry_1.add_to_hass(hass) + + # Create device with three config subentries + device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-2", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry_1.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert device_entry.config_entries == {config_entry_1.entry_id} + assert device_entry.config_entries_subentries == { + config_entry_1.entry_id: {None, "mock-subentry-id-1", "mock-subentry-id-2"}, + } + + # Create an entity without config entry or subentry + entry_1 = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + device_id=device_entry.id, + ) + + assert entity_registry.async_is_registered(entry_1.entity_id) + + # Remove the first config subentry from the device + device_registry.async_update_device( + device_entry.id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id=None, + ) + await hass.async_block_till_done() + + assert device_registry.async_get(device_entry.id) + assert entity_registry.async_is_registered(entry_1.entity_id) + + # Remove the second config subentry from the device + device_registry.async_update_device( + device_entry.id, + remove_config_entry_id=config_entry_1.entry_id, + remove_config_subentry_id="mock-subentry-id-1", + ) + await hass.async_block_till_done() + + assert device_registry.async_get(device_entry.id) + assert entity_registry.async_is_registered(entry_1.entity_id) + + async def test_update_device_race( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -1741,6 +2117,35 @@ def test_entity_registry_items() -> None: assert entities.get_entry(entry2.id) is None +async def test_config_entry_does_not_exist(entity_registry: er.EntityRegistry) -> None: + """Test adding an entity linked to an unknown config entry.""" + mock_config = MockConfigEntry( + domain="light", + entry_id="mock-id-1", + pref_disable_new_entities=True, + ) + with pytest.raises(ValueError): + entity_registry.async_get_or_create( + "light", "hue", "1234", config_entry=mock_config + ) + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(ValueError): + entity_registry.async_update_entity( + entity_id, config_entry_id=mock_config.entry_id + ) + + +async def test_device_does_not_exist(entity_registry: er.EntityRegistry) -> None: + """Test adding an entity linked to an unknown device.""" + with pytest.raises(ValueError): + entity_registry.async_get_or_create("light", "hue", "1234", device_id="blah") + + entity_id = entity_registry.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(ValueError): + entity_registry.async_update_entity(entity_id, device_id="blah") + + async def test_disabled_by_str_not_allowed(entity_registry: er.EntityRegistry) -> None: """Test we need to pass disabled by type.""" with pytest.raises(ValueError): @@ -1813,11 +2218,46 @@ async def test_unique_id_non_string( ) +@pytest.mark.parametrize( + ("create_kwargs", "migrate_kwargs", "new_subentry_id"), + [ + ({}, {}, None), + ({"config_subentry_id": None}, {}, None), + ({}, {"new_config_subentry_id": None}, None), + ({}, {"new_config_subentry_id": "mock-subentry-id-2"}, "mock-subentry-id-2"), + ( + {"config_subentry_id": "mock-subentry-id-1"}, + {"new_config_subentry_id": None}, + None, + ), + ( + {"config_subentry_id": "mock-subentry-id-1"}, + {"new_config_subentry_id": "mock-subentry-id-2"}, + "mock-subentry-id-2", + ), + ], +) def test_migrate_entity_to_new_platform( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + create_kwargs: dict, + migrate_kwargs: dict, + new_subentry_id: str | None, ) -> None: """Test migrate_entity_to_new_platform.""" - orig_config_entry = MockConfigEntry(domain="light") + orig_config_entry = MockConfigEntry( + domain="light", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + orig_config_entry.add_to_hass(hass) orig_unique_id = "5678" orig_entry = entity_registry.async_get_or_create( @@ -1831,6 +2271,7 @@ def test_migrate_entity_to_new_platform( original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", + **create_kwargs, ) assert entity_registry.async_get("light.light") is orig_entry entity_registry.async_update_entity( @@ -1839,7 +2280,19 @@ def test_migrate_entity_to_new_platform( icon="new_icon", ) - new_config_entry = MockConfigEntry(domain="light") + new_config_entry = MockConfigEntry( + domain="light", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + new_config_entry.add_to_hass(hass) new_unique_id = "1234" assert entity_registry.async_update_entity_platform( @@ -1847,6 +2300,7 @@ def test_migrate_entity_to_new_platform( "hue2", new_unique_id=new_unique_id, new_config_entry_id=new_config_entry.entry_id, + **migrate_kwargs, ) assert not entity_registry.async_get_entity_id("light", "hue", orig_unique_id) @@ -1854,6 +2308,7 @@ def test_migrate_entity_to_new_platform( assert (new_entry := entity_registry.async_get("light.light")) is not orig_entry assert new_entry.config_entry_id == new_config_entry.entry_id + assert new_entry.config_subentry_id == new_subentry_id assert new_entry.unique_id == new_unique_id assert new_entry.name == "new_name" assert new_entry.icon == "new_icon" @@ -1886,6 +2341,99 @@ def test_migrate_entity_to_new_platform( ) +def test_migrate_entity_to_new_platform_error_handling( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test migrate_entity_to_new_platform.""" + orig_config_entry = MockConfigEntry( + domain="light", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + orig_config_entry.add_to_hass(hass) + orig_unique_id = "5678" + + orig_entry = entity_registry.async_get_or_create( + "light", + "hue", + orig_unique_id, + suggested_object_id="light", + config_entry=orig_config_entry, + config_subentry_id="mock-subentry-id-1", + disabled_by=er.RegistryEntryDisabler.USER, + entity_category=EntityCategory.CONFIG, + original_device_class="mock-device-class", + original_icon="initial-original_icon", + original_name="initial-original_name", + ) + assert entity_registry.async_get("light.light") is orig_entry + + new_config_entry = MockConfigEntry( + domain="light", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + new_config_entry.add_to_hass(hass) + new_unique_id = "1234" + + # Test migrating nonexisting entity + with pytest.raises(KeyError, match="'light.not_a_real_light'"): + entity_registry.async_update_entity_platform( + "light.not_a_real_light", + "hue2", + new_unique_id=new_unique_id, + new_config_entry_id=new_config_entry.entry_id, + ) + + # Test migrate entity without new config entry ID + with pytest.raises( + ValueError, + match="new_config_entry_id required because light.light is already linked to a config entry", + ): + entity_registry.async_update_entity_platform( + "light.light", + "hue3", + ) + + # Test migrate entity without new config subentry ID + with pytest.raises( + ValueError, + match="Can't change config entry without changing subentry", + ): + entity_registry.async_update_entity_platform( + "light.light", + "hue3", + new_config_entry_id=new_config_entry.entry_id, + ) + + # Test entity with a state + hass.states.async_set("light.light", "on") + with pytest.raises( + ValueError, match="Only entities that haven't been loaded can be migrated" + ): + entity_registry.async_update_entity_platform( + "light.light", + "hue2", + new_unique_id=new_unique_id, + new_config_entry_id=new_config_entry.entry_id, + ) + + async def test_restore_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -1893,12 +2441,28 @@ async def test_restore_entity( ) -> None: """Make sure entity registry id is stable and entity_id is reused if possible.""" update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) - config_entry = MockConfigEntry(domain="light") + config_entry = MockConfigEntry( + domain="light", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + config_entry.add_to_hass(hass) entry1 = entity_registry.async_get_or_create( "light", "hue", "1234", config_entry=config_entry ) entry2 = entity_registry.async_get_or_create( - "light", "hue", "5678", config_entry=config_entry + "light", + "hue", + "5678", + config_entry=config_entry, + config_subentry_id="mock-subentry-id-1-1", ) entry1 = entity_registry.async_update_entity( @@ -1922,8 +2486,11 @@ async def test_restore_entity( # entity_id is not restored assert attr.evolve(entry1, entity_id="light.hue_1234") == entry1_restored assert entry2 != entry2_restored - # Config entry is not restored - assert attr.evolve(entry2, config_entry_id=None) == entry2_restored + # Config entry and subentry are not restored + assert ( + attr.evolve(entry2, config_entry_id=None, config_subentry_id=None) + == entry2_restored + ) # Remove two of the entities again, then bump time entity_registry.async_remove(entry1_restored.entity_id) @@ -1988,6 +2555,8 @@ async def test_async_migrate_entry_delete_self( """Test async_migrate_entry.""" config_entry1 = MockConfigEntry(domain="test1") config_entry2 = MockConfigEntry(domain="test2") + config_entry1.add_to_hass(hass) + config_entry2.add_to_hass(hass) entry1 = entity_registry.async_get_or_create( "light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1" ) @@ -2023,6 +2592,8 @@ async def test_async_migrate_entry_delete_other( """Test async_migrate_entry.""" config_entry1 = MockConfigEntry(domain="test1") config_entry2 = MockConfigEntry(domain="test2") + config_entry1.add_to_hass(hass) + config_entry2.add_to_hass(hass) entry1 = entity_registry.async_get_or_create( "light", "hue", "1234", config_entry=config_entry1, original_name="Entry 1" ) @@ -2230,3 +2801,132 @@ async def test_async_remove_thread_safety( match="Detected code that calls entity_registry.async_remove from a thread.", ): await hass.async_add_executor_job(entity_registry.async_remove, entry.entity_id) + + +async def test_subentry( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """Test subentry error handling.""" + entry1 = MockConfigEntry( + domain="light", + entry_id="mock-id-1", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ], + ) + entry1.add_to_hass(hass) + entry2 = MockConfigEntry( + domain="light", + entry_id="mock-id-2", + subentries_data=[ + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-2-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ) + ], + ) + entry2.add_to_hass(hass) + + with pytest.raises( + ValueError, match="Config entry mock-id-1 has no subentry bad-subentry-id" + ): + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry1, + config_subentry_id="bad-subentry-id", + ) + + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry1, + config_subentry_id="mock-subentry-id-1-1", + ) + assert entry.config_subentry_id == "mock-subentry-id-1-1" + + # Try updating subentry + with pytest.raises( + ValueError, match="Config entry mock-id-1 has no subentry bad-subentry-id" + ): + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry1, + config_subentry_id="bad-subentry-id", + ) + + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry1, + config_subentry_id="mock-subentry-id-1-2", + ) + assert entry.config_subentry_id == "mock-subentry-id-1-2" + + with pytest.raises( + ValueError, match="Can't change config entry without changing subentry" + ): + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry2, + ) + + with pytest.raises( + ValueError, match="Config entry mock-id-2 has no subentry mock-subentry-id-1-2" + ): + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry2, + config_subentry_id="mock-subentry-id-1-2", + ) + + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry2, + config_subentry_id="mock-subentry-id-2-1", + ) + assert entry.config_subentry_id == "mock-subentry-id-2-1" + + entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=entry1, + config_subentry_id=None, + ) + assert entry.config_subentry_id is None + + entry = entity_registry.async_update_entity( + entry.entity_id, + config_entry_id=entry2.entry_id, + config_subentry_id="mock-subentry-id-2-1", + ) + assert entry.config_subentry_id == "mock-subentry-id-2-1" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index a0014587cd0..a8691771580 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -13,8 +13,8 @@ from freezegun.api import FrozenDateTimeFactory import jinja2 import pytest +from homeassistant import core as ha from homeassistant.const import MATCH_ALL -import homeassistant.core as ha from homeassistant.core import ( Event, EventStateChangedData, @@ -52,7 +52,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.template import Template, result_as_boolean from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, async_fire_time_changed_exact diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index 684778fe1b1..4b9f2fa2bf6 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -7,7 +7,7 @@ import pytest from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant -import homeassistant.helpers.httpx_client as client +from homeassistant.helpers import httpx_client as client from tests.common import MockModule, extract_stack_to_frame, mock_integration diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index ae8c2ed65d0..bf0df305c35 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -234,7 +234,7 @@ async def test_async_match_targets( # Floor 2 floor_2 = floor_registry.async_create("second floor", aliases={"upstairs"}) - area_bedroom_2 = area_registry.async_get_or_create("bedroom") + area_bedroom_2 = area_registry.async_get_or_create("second floor bedroom") area_bedroom_2 = area_registry.async_update( area_bedroom_2.id, floor_id=floor_2.floor_id ) @@ -269,7 +269,7 @@ async def test_async_match_targets( # Floor 3 floor_3 = floor_registry.async_create("third floor", aliases={"upstairs"}) - area_bedroom_3 = area_registry.async_get_or_create("bedroom") + area_bedroom_3 = area_registry.async_get_or_create("third floor bedroom") area_bedroom_3 = area_registry.async_update( area_bedroom_3.id, floor_id=floor_3.floor_id ) @@ -510,6 +510,37 @@ async def test_async_match_targets( bathroom_light_3.entity_id, } + # Check single target constraint + result = intent.async_match_targets( + hass, + intent.MatchTargetsConstraints(domains={"light"}, single_target=True), + states=states, + ) + assert not result.is_match + assert result.no_match_reason == intent.MatchFailedReason.MULTIPLE_TARGETS + + # Only one light on the ground floor + result = intent.async_match_targets( + hass, + intent.MatchTargetsConstraints(domains={"light"}, single_target=True), + preferences=intent.MatchTargetsPreferences(floor_id=floor_1.floor_id), + states=states, + ) + assert result.is_match + assert len(result.states) == 1 + assert result.states[0].entity_id == bathroom_light_1.entity_id + + # Only one switch in bedroom + result = intent.async_match_targets( + hass, + intent.MatchTargetsConstraints(domains={"switch"}, single_target=True), + preferences=intent.MatchTargetsPreferences(area_id=area_bedroom_2.id), + states=states, + ) + assert result.is_match + assert len(result.states) == 1 + assert result.states[0].entity_id == bedroom_switch_2.entity_id + async def test_match_device_area( hass: HomeAssistant, diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 3787526c433..630ed3f4fa1 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -1,15 +1,17 @@ """Tests for the llm helpers.""" +from datetime import timedelta from decimal import Decimal from unittest.mock import patch import pytest import voluptuous as vol +from homeassistant.components import calendar from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.intent import async_register_timer_handler from homeassistant.components.script.config import ScriptConfig -from homeassistant.core import Context, HomeAssistant, State +from homeassistant.core import Context, HomeAssistant, State, SupportsResponse from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( area_registry as ar, @@ -22,8 +24,9 @@ from homeassistant.helpers import ( selector, ) from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_mock_service @pytest.fixture @@ -39,6 +42,14 @@ def llm_context() -> llm.LLMContext: ) +class MyAPI(llm.API): + """Test API.""" + + async def async_get_api_instance(self, _: llm.ToolInput) -> llm.APIInstance: + """Return a list of tools.""" + return llm.APIInstance(self, "", [], llm_context) + + async def test_get_api_no_existing( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: @@ -50,11 +61,6 @@ async def test_get_api_no_existing( async def test_register_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> None: """Test registering an llm api.""" - class MyAPI(llm.API): - async def async_get_api_instance(self, _: llm.ToolInput) -> llm.APIInstance: - """Return a list of tools.""" - return llm.APIInstance(self, "", [], llm_context) - api = MyAPI(hass=hass, id="test", name="Test") llm.async_register_api(hass, api) @@ -66,6 +72,59 @@ async def test_register_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> llm.async_register_api(hass, api) +async def test_unregister_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> None: + """Test unregistering an llm api.""" + + unreg = llm.async_register_api(hass, MyAPI(hass=hass, id="test", name="Test")) + assert await llm.async_get_api(hass, "test", llm_context) + unreg() + with pytest.raises(HomeAssistantError): + assert await llm.async_get_api(hass, "test", llm_context) + + +async def test_reregister_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> None: + """Test unregistering an llm api then re-registering with the same id.""" + + unreg = llm.async_register_api(hass, MyAPI(hass=hass, id="test", name="Test")) + assert await llm.async_get_api(hass, "test", llm_context) + unreg() + llm.async_register_api(hass, MyAPI(hass=hass, id="test", name="Test")) + assert await llm.async_get_api(hass, "test", llm_context) + + +async def test_unregister_twice( + hass: HomeAssistant, llm_context: llm.LLMContext +) -> None: + """Test unregistering an llm api twice.""" + + unreg = llm.async_register_api(hass, MyAPI(hass=hass, id="test", name="Test")) + assert await llm.async_get_api(hass, "test", llm_context) + unreg() + + # Unregistering twice is a bug that should not happen + with pytest.raises(KeyError): + unreg() + + +async def test_multiple_apis(hass: HomeAssistant, llm_context: llm.LLMContext) -> None: + """Test registering multiple APIs.""" + + unreg1 = llm.async_register_api(hass, MyAPI(hass=hass, id="test-1", name="Test 1")) + llm.async_register_api(hass, MyAPI(hass=hass, id="test-2", name="Test 2")) + + # Verify both Apis are registered + assert await llm.async_get_api(hass, "test-1", llm_context) + assert await llm.async_get_api(hass, "test-2", llm_context) + + # Unregister and verify only one is left + unreg1() + + with pytest.raises(HomeAssistantError): + assert await llm.async_get_api(hass, "test-1", llm_context) + + assert await llm.async_get_api(hass, "test-2", llm_context) + + async def test_call_tool_no_existing( hass: HomeAssistant, llm_context: llm.LLMContext ) -> None: @@ -686,7 +745,7 @@ async def test_script_tool( area = area_registry.async_create("Living room") floor = floor_registry.async_create("2") - assert llm.SCRIPT_PARAMETERS_CACHE not in hass.data + assert llm.ACTION_PARAMETERS_CACHE not in hass.data api = await llm.async_get_api(hass, "assist", llm_context) @@ -710,7 +769,7 @@ async def test_script_tool( } assert tool.parameters.schema == schema - assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { + assert hass.data[llm.ACTION_PARAMETERS_CACHE]["script"] == { "test_script": ( "This is a test script. Aliases: ['script name', 'script alias']", vol.Schema(schema), @@ -807,7 +866,7 @@ async def test_script_tool( ): await hass.services.async_call("script", "reload", blocking=True) - assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == {} + assert hass.data[llm.ACTION_PARAMETERS_CACHE]["script"] == {} api = await llm.async_get_api(hass, "assist", llm_context) @@ -823,7 +882,7 @@ async def test_script_tool( schema = {vol.Required("beer", description="Number of beers"): cv.string} assert tool.parameters.schema == schema - assert hass.data[llm.SCRIPT_PARAMETERS_CACHE] == { + assert hass.data[llm.ACTION_PARAMETERS_CACHE]["script"] == { "test_script": ( "This is a new test script. Aliases: ['script name', 'script alias']", vol.Schema(schema), @@ -1106,3 +1165,105 @@ async def test_selector_serializer( assert selector_serializer(selector.FileSelector({"accept": ".txt"})) == { "type": "string" } + + +async def test_calendar_get_events_tool(hass: HomeAssistant) -> None: + """Test the calendar get events tool.""" + assert await async_setup_component(hass, "homeassistant", {}) + hass.states.async_set( + "calendar.test_calendar", "on", {"friendly_name": "Mock Calendar Name"} + ) + async_expose_entity(hass, "conversation", "calendar.test_calendar", True) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + user_prompt="test_text", + language="*", + assistant="conversation", + device_id=None, + ) + api = await llm.async_get_api(hass, "assist", llm_context) + tool = next( + (tool for tool in api.tools if tool.name == "calendar_get_events"), None + ) + assert tool is not None + assert tool.parameters.schema["calendar"].container == ["Mock Calendar Name"] + + calls = async_mock_service( + hass, + domain=calendar.DOMAIN, + service=calendar.SERVICE_GET_EVENTS, + schema=calendar.SERVICE_GET_EVENTS_SCHEMA, + response={ + "calendar.test_calendar": { + "events": [ + { + "start": "2025-09-17", + "end": "2025-09-18", + "summary": "Home Assistant 12th birthday", + "description": "", + }, + { + "start": "2025-09-17T14:00:00-05:00", + "end": "2025-09-18T15:00:00-05:00", + "summary": "Champagne", + "description": "", + }, + ] + } + }, + supports_response=SupportsResponse.ONLY, + ) + + tool_input = llm.ToolInput( + tool_name="calendar_get_events", + tool_args={ + "calendar": "Mock Calendar Name", + "range": "today", + }, + ) + now = dt_util.now() + with patch("homeassistant.util.dt.now", return_value=now): + response = await api.async_call_tool(tool_input) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == calendar.DOMAIN + assert call.service == calendar.SERVICE_GET_EVENTS + assert call.data == { + "entity_id": ["calendar.test_calendar"], + "start_date_time": now, + "end_date_time": dt_util.start_of_local_day() + timedelta(days=1), + } + + assert response == { + "success": True, + "result": [ + { + "start": "2025-09-17", + "end": "2025-09-18", + "summary": "Home Assistant 12th birthday", + "description": "", + "all_day": True, + }, + { + "start": "2025-09-17T14:00:00-05:00", + "end": "2025-09-18T15:00:00-05:00", + "summary": "Champagne", + "description": "", + }, + ], + } + + tool_input.tool_args["range"] = "week" + with patch("homeassistant.util.dt.now", return_value=now): + response = await api.async_call_tool(tool_input) + + assert len(calls) == 2 + call = calls[1] + assert call.data == { + "entity_id": ["calendar.test_calendar"], + "start_date_time": now, + "end_date_time": dt_util.start_of_local_day() + timedelta(days=7), + } diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c438e333ae6..f3cbb982ad0 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -44,7 +44,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import UNDEFINED from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from tests.common import ( MockConfigEntry, @@ -4118,6 +4118,14 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: } ], }, + { + "sequence": [ + { + "action": "test.script", + "data": {"label_id": "label_sequence"}, + } + ], + }, ] ), "Test Name", @@ -4135,6 +4143,7 @@ async def test_referenced_labels(hass: HomeAssistant) -> None: "label_if_then", "label_if_else", "label_parallel", + "label_sequence", } # Test we cache results. assert script_obj.referenced_labels is script_obj.referenced_labels @@ -4220,6 +4229,14 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: } ], }, + { + "sequence": [ + { + "action": "test.script", + "data": {"floor_id": "floor_sequence"}, + } + ], + }, ] ), "Test Name", @@ -4236,6 +4253,7 @@ async def test_referenced_floors(hass: HomeAssistant) -> None: "floor_if_then", "floor_if_else", "floor_parallel", + "floor_sequence", } # Test we cache results. assert script_obj.referenced_floors is script_obj.referenced_floors @@ -4321,6 +4339,14 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: } ], }, + { + "sequence": [ + { + "action": "test.script", + "data": {"area_id": "area_sequence"}, + } + ], + }, ] ), "Test Name", @@ -4337,6 +4363,7 @@ async def test_referenced_areas(hass: HomeAssistant) -> None: "area_if_then", "area_if_else", "area_parallel", + "area_sequence", # 'area_service_template', # no area extraction from template } # Test we cache results. @@ -4437,6 +4464,14 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: } ], }, + { + "sequence": [ + { + "action": "test.script", + "data": {"entity_id": "light.sequence"}, + } + ], + }, ] ), "Test Name", @@ -4456,6 +4491,7 @@ async def test_referenced_entities(hass: HomeAssistant) -> None: "light.if_then", "light.if_else", "light.parallel", + "light.sequence", # "light.service_template", # no entity extraction from template "scene.hello", "sensor.condition", @@ -4554,6 +4590,14 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: } ], }, + { + "sequence": [ + { + "action": "test.script", + "target": {"device_id": "sequence-device"}, + } + ], + }, ] ), "Test Name", @@ -4575,6 +4619,7 @@ async def test_referenced_devices(hass: HomeAssistant) -> None: "if-then", "if-else", "parallel-device", + "sequence-device", } # Test we cache results. assert script_obj.referenced_devices is script_obj.referenced_devices diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index f73808a0625..d07bb7458e9 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -7,7 +7,7 @@ import pytest import voluptuous as vol from homeassistant.helpers import selector -from homeassistant.util import yaml +from homeassistant.util import yaml as yaml_util FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @@ -77,7 +77,7 @@ def _test_selector( "selector": {selector_type: selector_instance.config} } # Test serialized selector can be dumped to YAML - yaml.dump(selector_instance.serialize()) + yaml_util.dump(selector_instance.serialize()) @pytest.mark.parametrize( diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 6d03e09cdf7..142f7a23f81 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -36,11 +36,11 @@ from homeassistant.core import ( ) from homeassistant.helpers import ( area_registry as ar, + config_validation as cv, device_registry as dr, entity_registry as er, service, ) -import homeassistant.helpers.config_validation as cv from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util.yaml.loader import parse_yaml @@ -122,6 +122,8 @@ def floor_area_mock(hass: HomeAssistant) -> None: floor_id="test-floor", icon=None, picture=None, + temperature_entity_id=None, + humidity_entity_id=None, ) area_in_floor_a = ar.AreaEntry( id="area-a", @@ -130,6 +132,8 @@ def floor_area_mock(hass: HomeAssistant) -> None: floor_id="floor-a", icon=None, picture=None, + temperature_entity_id=None, + humidity_entity_id=None, ) mock_area_registry( hass, @@ -284,6 +288,8 @@ def label_mock(hass: HomeAssistant) -> None: icon=None, labels={"label_area"}, picture=None, + temperature_entity_id=None, + humidity_entity_id=None, ) area_without_labels = ar.AreaEntry( id="area-no-labels", @@ -293,6 +299,8 @@ def label_mock(hass: HomeAssistant) -> None: icon=None, labels=set(), picture=None, + temperature_entity_id=None, + humidity_entity_id=None, ) mock_area_registry( hass, diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index 54c26997422..973d68b1f5c 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -10,7 +10,7 @@ import pytest from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant from homeassistant.helpers import sun -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util def test_next_events(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index 2c4b95302fc..ad140834199 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -9,17 +9,7 @@ import pytest from homeassistant.components import hassio from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant -from homeassistant.helpers.system_info import async_get_system_info, is_official_image - - -async def test_is_official_image() -> None: - """Test is_official_image.""" - is_official_image.cache_clear() - with patch("homeassistant.helpers.system_info.os.path.isfile", return_value=True): - assert is_official_image() is True - is_official_image.cache_clear() - with patch("homeassistant.helpers.system_info.os.path.isfile", return_value=False): - assert is_official_image() is False +from homeassistant.helpers.system_info import async_get_system_info async def test_get_system_info(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 628aea20900..b3a30806cbd 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -50,7 +50,7 @@ from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.json import json_dumps from homeassistant.helpers.typing import TemplateVarsType from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.unit_system import UnitSystem @@ -1970,7 +1970,7 @@ def test_is_state(hass: HomeAssistant) -> None: def test_is_state_attr(hass: HomeAssistant) -> None: """Test is_state_attr method.""" - hass.states.async_set("test.object", "available", {"mode": "on"}) + hass.states.async_set("test.object", "available", {"mode": "on", "exists": None}) tpl = template.Template( """ {% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %} @@ -2003,6 +2003,22 @@ def test_is_state_attr(hass: HomeAssistant) -> None: ) assert tpl.async_render() == "test.object" + tpl = template.Template( + """ +{% if is_state_attr("test.object", "exists", None) %}yes{% else %}no{% endif %} + """, + hass, + ) + assert tpl.async_render() == "yes" + + tpl = template.Template( + """ +{% if is_state_attr("test.object", "noexist", None) %}yes{% else %}no{% endif %} + """, + hass, + ) + assert tpl.async_render() == "no" + def test_state_attr(hass: HomeAssistant) -> None: """Test state_attr method.""" @@ -2110,6 +2126,7 @@ async def test_state_translated( hass.states.async_set("domain.is_unknown", "unknown", attributes={}) config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) entity_registry.async_get_or_create( "light", "hue", diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index d4a78807e2b..3593db9cf87 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -74,7 +74,7 @@ def test_load_translations_files_by_language( "name": "Other 4", "unit_of_measurement": "quantities", }, - "outlet": {"name": "Outlet " "{placeholder}"}, + "outlet": {"name": "Outlet {placeholder}"}, } }, "something": "else", diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 539762a60ff..3ad5754dada 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging from unittest.mock import AsyncMock, Mock, patch import urllib.error +import weakref import aiohttp from freezegun.api import FrozenDateTimeFactory @@ -12,7 +13,7 @@ import requests from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, @@ -898,3 +899,41 @@ async def test_config_entry(hass: HomeAssistant) -> None: hass, _LOGGER, name="test", config_entry=another_entry ) assert crd.config_entry is another_entry + + +async def test_listener_unsubscribe_releases_coordinator(hass: HomeAssistant) -> None: + """Test listener subscribe/unsubscribe releases parent class. + + See https://github.com/home-assistant/core/issues/137237 + And https://github.com/home-assistant/core/pull/137338 + """ + + class Subscriber: + _unsub: CALLBACK_TYPE | None = None + + def start_listen( + self, coordinator: update_coordinator.DataUpdateCoordinator + ) -> None: + self._unsub = coordinator.async_add_listener(lambda: None) + + def stop_listen(self) -> None: + self._unsub() + self._unsub = None + + coordinator = update_coordinator.DataUpdateCoordinator[int]( + hass, _LOGGER, name="test" + ) + subscriber = Subscriber() + subscriber.start_listen(coordinator) + + # Keep weak reference to the coordinator + weak_ref = weakref.ref(coordinator) + assert weak_ref() is not None + + # Unload the subscriber, then shutdown the coordinator + subscriber.stop_listen() + await coordinator.async_shutdown() + del coordinator + + # Ensure the coordinator is released + assert weak_ref() is None diff --git a/tests/patch_recorder.py b/tests/patch_recorder.py index 4993e84fc30..e0e66de19a5 100644 --- a/tests/patch_recorder.py +++ b/tests/patch_recorder.py @@ -6,7 +6,7 @@ from contextlib import contextmanager import sys # Patch recorder util session scope -from homeassistant.helpers import recorder as recorder_helper # noqa: E402 +from homeassistant.helpers import recorder as recorder_helper # Make sure homeassistant.components.recorder.util is not already imported assert "homeassistant.components.recorder.util" not in sys.modules diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 6c53e9832d9..efa3ca9523a 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1370,7 +1370,7 @@ def test_valid_generic( async def async_setup_entry( #@ hass: HomeAssistant, entry: {entry_annotation}, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: pass """, @@ -1402,7 +1402,7 @@ def test_invalid_generic( async def async_setup_entry( #@ hass: HomeAssistant, entry: {entry_annotation}, #@ - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: pass """, diff --git a/tests/ruff.toml b/tests/ruff.toml index bbfbfe1305d..c56b8f68ffc 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -10,6 +10,10 @@ extend-ignore = [ "SLF001", # Private member accessed: Tests do often test internals a lot ] +[lint.flake8-tidy-imports.banned-api] +"async_timeout".msg = "use asyncio.timeout instead" +"pytz".msg = "use zoneinfo instead" + [lint.isort] known-first-party = [ "homeassistant", diff --git a/tests/syrupy.py b/tests/syrupy.py index a3b3f763063..3c8e398f0f8 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -109,6 +109,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): serializable_data = cls._serializable_issue_registry_entry(data) elif isinstance(data, dict) and "flow_id" in data and "handler" in data: serializable_data = cls._serializable_flow_result(data) + elif isinstance(data, dict) and set(data) == {"conversation_id", "response"}: + serializable_data = cls._serializable_conversation_result(data) elif isinstance(data, vol.Schema): serializable_data = voluptuous_serialize.convert(data) elif isinstance(data, ConfigEntry): @@ -158,6 +160,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): attrs.asdict(data) | { "config_entries": ANY, + "config_entries_subentries": ANY, "id": ANY, } ) @@ -186,6 +189,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): attrs.asdict(data) | { "config_entry_id": ANY, + "config_subentry_id": ANY, "device_id": ANY, "id": ANY, "options": {k: dict(v) for k, v in data.options.items()}, @@ -200,6 +204,11 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): """Prepare a Home Assistant flow result for serialization.""" return FlowResultSnapshot(data | {"flow_id": ANY}) + @classmethod + def _serializable_conversation_result(cls, data: dict) -> SerializableData: + """Prepare a Home Assistant conversation result for serialization.""" + return data | {"conversation_id": ANY} + @classmethod def _serializable_issue_registry_entry( cls, data: ir.IssueEntry @@ -376,7 +385,7 @@ def override_syrupy_finish(self: SnapshotSession) -> int: with open(".pytest_syrupy_worker_count", "w", encoding="utf-8") as f: f.write(os.getenv("PYTEST_XDIST_WORKER_COUNT")) with open( - f".pytest_syrupy_{os.getenv("PYTEST_XDIST_WORKER")}_result", + f".pytest_syrupy_{os.getenv('PYTEST_XDIST_WORKER')}_result", "w", encoding="utf-8", ) as f: diff --git a/tests/test_backup_restore.py b/tests/test_backup_restore.py index bce5eca4292..4c6bc930667 100644 --- a/tests/test_backup_restore.py +++ b/tests/test_backup_restore.py @@ -1,7 +1,10 @@ """Test methods in backup_restore.""" +from collections.abc import Generator +import json from pathlib import Path import tarfile +from typing import Any from unittest import mock import pytest @@ -11,6 +14,23 @@ from homeassistant import backup_restore from .common import get_test_config_dir +@pytest.fixture(autouse=True) +def remove_restore_result_file() -> Generator[None, Any, Any]: + """Remove the restore result file.""" + yield + Path(get_test_config_dir(".HA_RESTORE_RESULT")).unlink(missing_ok=True) + + +def restore_result_file_content() -> dict[str, Any] | None: + """Return the content of the restore result file.""" + try: + return json.loads( + Path(get_test_config_dir(".HA_RESTORE_RESULT")).read_text("utf-8") + ) + except FileNotFoundError: + return None + + @pytest.mark.parametrize( ("side_effect", "content", "expected"), [ @@ -57,11 +77,14 @@ def test_reading_the_instruction_contents( return_value=content, side_effect=side_effect, ), + mock.patch("pathlib.Path.unlink", autospec=True) as unlink_mock, ): - read_content = backup_restore.restore_backup_file_content( - Path(get_test_config_dir()) - ) + config_path = Path(get_test_config_dir()) + read_content = backup_restore.restore_backup_file_content(config_path) assert read_content == expected + unlink_mock.assert_called_once_with( + config_path / ".HA_RESTORE", missing_ok=True + ) def test_restoring_backup_that_does_not_exist() -> None: @@ -84,6 +107,11 @@ def test_restoring_backup_that_does_not_exist() -> None: ), ): assert backup_restore.restore_backup(Path(get_test_config_dir())) is False + assert restore_result_file_content() == { + "error": f"Backup file {backup_file_path} does not exist", + "error_type": "ValueError", + "success": False, + } def test_restoring_backup_when_instructions_can_not_be_read() -> None: @@ -95,6 +123,7 @@ def test_restoring_backup_when_instructions_can_not_be_read() -> None: ), ): assert backup_restore.restore_backup(Path(get_test_config_dir())) is False + assert restore_result_file_content() is None def test_restoring_backup_that_is_not_a_file() -> None: @@ -118,6 +147,11 @@ def test_restoring_backup_that_is_not_a_file() -> None: ), ): assert backup_restore.restore_backup(Path(get_test_config_dir())) is False + assert restore_result_file_content() == { + "error": f"Backup file {backup_file_path} does not exist", + "error_type": "ValueError", + "success": False, + } def test_aborting_for_older_versions() -> None: @@ -149,6 +183,13 @@ def test_aborting_for_older_versions() -> None: ), ): assert backup_restore.restore_backup(config_dir) is True + assert restore_result_file_content() == { + "error": ( + "You need at least Home Assistant version 9999.99.99 to restore this backup" + ), + "error_type": "ValueError", + "success": False, + } @pytest.mark.parametrize( @@ -277,6 +318,11 @@ def test_removal_of_current_configuration_when_restoring( assert removed_directories == { Path(config_dir, d) for d in expected_removed_directories } + assert restore_result_file_content() == { + "error": None, + "error_type": None, + "success": True, + } def test_extracting_the_contents_of_a_backup_file() -> None: @@ -329,6 +375,11 @@ def test_extracting_the_contents_of_a_backup_file() -> None: assert { member.name for member in extractall_mock.mock_calls[-1].kwargs["members"] } == {"data", "data/.HA_VERSION", "data/.storage", "data/www"} + assert restore_result_file_content() == { + "error": None, + "error_type": None, + "success": True, + } @pytest.mark.parametrize( @@ -359,6 +410,11 @@ def test_remove_backup_file_after_restore( assert mock_unlink.call_count == unlink_calls for call in mock_unlink.mock_calls: assert call.args[0] == backup_file_path + assert restore_result_file_content() == { + "error": None, + "error_type": None, + "success": True, + } @pytest.mark.parametrize( diff --git a/tests/test_block_async_io.py b/tests/test_block_async_io.py index dd23d4e9709..f42fbb9f4ef 100644 --- a/tests/test_block_async_io.py +++ b/tests/test_block_async_io.py @@ -261,7 +261,7 @@ async def test_protect_path_read_bytes(caplog: pytest.LogCaptureFixture) -> None block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - Path("/config/data_not_exist").read_bytes(), # noqa: ASYNC230 + Path("/config/data_not_exist").read_bytes(), ): pass @@ -274,7 +274,7 @@ async def test_protect_path_read_text(caplog: pytest.LogCaptureFixture) -> None: block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - Path("/config/data_not_exist").read_text(encoding="utf8"), # noqa: ASYNC230 + Path("/config/data_not_exist").read_text(encoding="utf8"), ): pass @@ -287,7 +287,7 @@ async def test_protect_path_write_bytes(caplog: pytest.LogCaptureFixture) -> Non block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - Path("/config/data/not/exist").write_bytes(b"xxx"), # noqa: ASYNC230 + Path("/config/data/not/exist").write_bytes(b"xxx"), ): pass @@ -300,7 +300,7 @@ async def test_protect_path_write_text(caplog: pytest.LogCaptureFixture) -> None block_async_io.enable() with ( contextlib.suppress(FileNotFoundError), - Path("/config/data/not/exist").write_text("xxx", encoding="utf8"), # noqa: ASYNC230 + Path("/config/data/not/exist").write_text("xxx", encoding="utf8"), ): pass diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index a32d7d1e50b..d554ca9449a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -12,8 +12,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import bootstrap, loader, runner -import homeassistant.config as config_util +from homeassistant import bootstrap, config as config_util, loader, runner from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( BASE_PLATFORMS, @@ -1091,7 +1090,7 @@ async def test_tasks_logged_that_block_stage_1( patch.object(bootstrap, "STAGE_1_TIMEOUT", 0), patch.object(bootstrap, "COOLDOWN_TIME", 0), patch.object( - bootstrap, "STAGE_1_INTEGRATIONS", [*original_stage_1, "normal_integration"] + bootstrap, "STAGE_1_INTEGRATIONS", {*original_stage_1, "normal_integration"} ), ): await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) @@ -1177,7 +1176,7 @@ async def test_bootstrap_is_cancellation_safe( @pytest.mark.parametrize("load_registries", [False]) async def test_bootstrap_empty_integrations(hass: HomeAssistant) -> None: """Test setting up an empty integrations does not raise.""" - await bootstrap.async_setup_multi_components(hass, set(), {}) + await bootstrap._async_setup_multi_components(hass, set(), {}) await hass.async_block_till_done() @@ -1312,7 +1311,7 @@ async def test_bootstrap_dependencies( ), ): bootstrap.async_set_domains_to_be_loaded(hass, {integration}) - await bootstrap.async_setup_multi_components(hass, {integration}, {}) + await bootstrap._async_setup_multi_components(hass, {integration}, {}) await hass.async_block_till_done() for assertion in assertions: @@ -1374,11 +1373,11 @@ async def test_pre_import_no_requirements(hass: HomeAssistant) -> None: @pytest.mark.timeout(20) -async def test_bootstrap_does_not_preload_stage_1_integrations() -> None: - """Test that the bootstrap does not preload stage 1 integrations. +async def test_bootstrap_does_not_preimport_stage_1_integrations() -> None: + """Test that the bootstrap does not preimport stage 1 integrations. If this test fails it means that stage1 integrations are being - loaded too soon and will not get their requirements updated + imported too soon and will not get their requirements updated before they are loaded at runtime. """ @@ -1404,7 +1403,7 @@ async def test_cancellation_does_not_leak_upward_from_async_setup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test setting up an integration that raises asyncio.CancelledError.""" - await bootstrap.async_setup_multi_components( + await bootstrap._async_setup_multi_components( hass, {"test_package_raises_cancelled_error"}, {} ) await hass.async_block_till_done() @@ -1425,12 +1424,12 @@ async def test_cancellation_does_not_leak_upward_from_async_setup_entry( domain="test_package_raises_cancelled_error_config_entry", data={} ) entry.add_to_hass(hass) - await bootstrap.async_setup_multi_components( + await bootstrap._async_setup_multi_components( hass, {"test_package_raises_cancelled_error_config_entry"}, {} ) await hass.async_block_till_done() - await bootstrap.async_setup_multi_components(hass, {"test_package"}, {}) + await bootstrap._async_setup_multi_components(hass, {"test_package"}, {}) await hass.async_block_till_done() assert ( "Error setting up entry Mock Title for test_package_raises_cancelled_error_config_entry" diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index dfdee65b2b0..d6e730aae5e 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -7,11 +7,8 @@ import pytest from homeassistant.bootstrap import ( CORE_INTEGRATIONS, - DEBUGGER_INTEGRATIONS, DEFAULT_INTEGRATIONS, - FRONTEND_INTEGRATIONS, - LOGGING_AND_HTTP_DEPS_INTEGRATIONS, - RECORDER_INTEGRATIONS, + STAGE_0_INTEGRATIONS, STAGE_1_INTEGRATIONS, ) @@ -21,11 +18,12 @@ from homeassistant.bootstrap import ( "component", sorted( { - *DEBUGGER_INTEGRATIONS, *CORE_INTEGRATIONS, - *LOGGING_AND_HTTP_DEPS_INTEGRATIONS, - *FRONTEND_INTEGRATIONS, - *RECORDER_INTEGRATIONS, + *( + domain + for name, domains, timeout in STAGE_0_INTEGRATIONS + for domain in domains + ), *STAGE_1_INTEGRATIONS, *DEFAULT_INTEGRATIONS, } diff --git a/tests/test_config.py b/tests/test_config.py index c8c5b081119..569af3238d0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -15,8 +15,7 @@ from syrupy.assertion import SnapshotAssertion import voluptuous as vol import yaml -from homeassistant import loader -import homeassistant.config as config_util +from homeassistant import config as config_util, loader from homeassistant.const import CONF_PACKAGES, __version__ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigValidationError, HomeAssistantError diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1ad152e8e42..bf2280790fa 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -17,7 +17,6 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.components import dhcp from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, @@ -40,14 +39,15 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import entity_registry as er, frame, issue_registry as ir from homeassistant.helpers.discovery_flow import DiscoveryKey -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.json import json_dumps +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.async_ import create_eager_task -import homeassistant.util.dt as dt_util from homeassistant.util.json import json_loads from .common import ( @@ -462,14 +462,22 @@ async def test_remove_entry( assert result return result - mock_remove_entry = AsyncMock(return_value=None) + remove_entry_calls = [] + + async def mock_remove_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> None: + """Mock removing an entry.""" + # Check that the entry is not yet removed from config entries + assert hass.config_entries.async_get_entry(entry.entry_id) + remove_entry_calls.append(None) entity = MockEntity(unique_id="1234", name="Test Entity") async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" async_add_entities([entity]) @@ -512,6 +520,7 @@ async def test_remove_entry( assert len(entity_registry.entities) == 1 entity_entry = list(entity_registry.entities.values())[0] assert entity_entry.config_entry_id == entry.entry_id + assert entity_entry.config_subentry_id is None # Remove entry result = await manager.async_remove("test2") @@ -521,7 +530,7 @@ async def test_remove_entry( assert result == {"require_restart": False} # Check the remove callback was invoked. - assert mock_remove_entry.call_count == 1 + assert len(remove_entry_calls) == 1 # Check that config entry was removed. assert manager.async_entry_ids() == ["test1", "test3"] @@ -535,6 +544,118 @@ async def test_remove_entry( assert not entity_entry_list +async def test_remove_subentry( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + entity_registry: er.EntityRegistry, +) -> None: + """Test that we can remove a subentry.""" + subentry_id = "blabla" + update_listener_calls = [] + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + await hass.config_entries.async_forward_entry_setups(entry, ["light"]) + return True + + mock_remove_entry = AsyncMock(return_value=None) + + entry_entity = MockEntity(unique_id="0001", name="Test Entry Entity") + subentry_entity = MockEntity(unique_id="0002", name="Test Subentry Entity") + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + async_add_entities([entry_entity]) + async_add_entities([subentry_entity], config_subentry_id=subentry_id) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry( + subentries_data=[ + config_entries.ConfigSubentryData( + data={"first": True}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="unique", + title="Mock title", + ) + ] + ) + + async def update_listener( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> None: + """Test function.""" + assert entry.subentries == {} + update_listener_calls.append(None) + + entry.add_update_listener(update_listener) + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Check entity states got added + assert hass.states.get("light.test_entry_entity") is not None + assert hass.states.get("light.test_subentry_entity") is not None + assert len(hass.states.async_all()) == 2 + + # Check entities got added to entity registry + assert len(entity_registry.entities) == 2 + entry_entity_entry = entity_registry.entities["light.test_entry_entity"] + assert entry_entity_entry.config_entry_id == entry.entry_id + assert entry_entity_entry.config_subentry_id is None + subentry_entity_entry = entity_registry.entities["light.test_subentry_entity"] + assert subentry_entity_entry.config_entry_id == entry.entry_id + assert subentry_entity_entry.config_subentry_id == subentry_id + + # Remove subentry + result = manager.async_remove_subentry(entry, subentry_id) + assert len(update_listener_calls) == 1 + await hass.async_block_till_done() + + # Check that remove went well + assert result is True + + # Check the remove callback was not invoked. + assert mock_remove_entry.call_count == 0 + + # Check that the config subentry was removed. + assert entry.subentries == {} + + # Check that entity state has been removed + assert hass.states.get("light.test_entry_entity") is not None + assert hass.states.get("light.test_subentry_entity") is None + assert len(hass.states.async_all()) == 1 + + # Check that entity registry entry has been removed + entity_entry_list = list(entity_registry.entities) + assert entity_entry_list == ["light.test_entry_entity"] + + # Try to remove the subentry again + with pytest.raises(config_entries.UnknownSubEntry): + manager.async_remove_subentry(entry, subentry_id) + assert len(update_listener_calls) == 1 + + async def test_remove_entry_non_unique_unique_id( hass: HomeAssistant, manager: config_entries.ConfigEntries, @@ -925,10 +1046,13 @@ async def test_saving_and_loading( await self.async_set_unique_id("unique") subentries = [ config_entries.ConfigSubentryData( - data={"foo": "bar"}, title="subentry 1" + data={"foo": "bar"}, subentry_type="test", title="subentry 1" ), config_entries.ConfigSubentryData( - data={"sun": "moon"}, title="subentry 2", unique_id="very_unique" + data={"sun": "moon"}, + subentry_type="test", + title="subentry 2", + unique_id="very_unique", ), ] return self.async_create_entry( @@ -1005,12 +1129,14 @@ async def test_saving_and_loading( { "data": {"foo": "bar"}, "subentry_id": ANY, + "subentry_type": "test", "title": "subentry 1", "unique_id": None, }, { "data": {"sun": "moon"}, "subentry_id": ANY, + "subentry_type": "test", "title": "subentry 2", "unique_id": "very_unique", }, @@ -1266,7 +1392,7 @@ async def test_discovery_notification( notifications = async_get_persistent_notifications(hass) assert "config_entry_discovery" not in notifications - # Start first discovery flow to assert that reconfigure notification fires + # Start first discovery flow to assert that discovery notification fires flow1 = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY} ) @@ -1538,6 +1664,63 @@ async def test_update_entry_options_and_trigger_listener( assert len(update_listener_calls) == 1 +async def test_updating_subentry_data( + manager: config_entries.ConfigEntries, freezer: FrozenDateTimeFactory +) -> None: + """Test that we can update an entry data.""" + created = dt_util.utcnow() + subentry_id = "blabla" + entry = MockConfigEntry( + subentries_data=[ + config_entries.ConfigSubentryData( + data={"first": True}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="unique", + title="Mock title", + ) + ] + ) + subentry = entry.subentries[subentry_id] + entry.add_to_manager(manager) + + assert len(manager.async_entries()) == 1 + assert manager.async_entries()[0] == entry + assert entry.created_at == created + assert entry.modified_at == created + + freezer.tick() + + assert manager.async_update_subentry(entry, subentry) is False + assert entry.subentries == { + subentry_id: config_entries.ConfigSubentry( + data={"first": True}, + subentry_id=subentry_id, + subentry_type="test", + title="Mock title", + unique_id="unique", + ) + } + assert entry.modified_at == created + assert manager.async_entries()[0].modified_at == created + + freezer.tick() + modified = dt_util.utcnow() + + assert manager.async_update_subentry(entry, subentry, data={"second": True}) is True + assert entry.subentries == { + subentry_id: config_entries.ConfigSubentry( + data={"second": True}, + subentry_id=subentry_id, + subentry_type="test", + title="Mock title", + unique_id="unique", + ) + } + assert entry.modified_at == modified + assert manager.async_entries()[0].modified_at == modified + + async def test_update_subentry_and_trigger_listener( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: @@ -1547,7 +1730,10 @@ async def test_update_subentry_and_trigger_listener( update_listener_calls = [] subentry = config_entries.ConfigSubentry( - data={"test": "test"}, unique_id="test", title="Mock title" + data={"test": "test"}, + subentry_type="test", + unique_id="test", + title="Mock title", ) async def update_listener( @@ -1566,12 +1752,27 @@ async def test_update_subentry_and_trigger_listener( assert entry.subentries == expected_subentries assert len(update_listener_calls) == 1 + assert ( + manager.async_update_subentry( + entry, + subentry, + data={"test": "test2"}, + title="New title", + unique_id="test2", + ) + is True + ) + + await hass.async_block_till_done(wait_background_tasks=True) + assert entry.subentries == expected_subentries + assert len(update_listener_calls) == 2 + expected_subentries = {} assert manager.async_remove_subentry(entry, subentry.subentry_id) is True await hass.async_block_till_done(wait_background_tasks=True) assert entry.subentries == expected_subentries - assert len(update_listener_calls) == 2 + assert len(update_listener_calls) == 3 async def test_setup_raise_not_ready( @@ -1914,6 +2115,7 @@ async def test_create_entry_subentries( subentrydata = config_entries.ConfigSubentryData( data={"test": "test"}, title="Mock title", + subentry_type="test", unique_id="test", ) @@ -1959,13 +2161,14 @@ async def test_create_entry_subentries( entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 - assert entries[0].supported_subentries == () + assert entries[0].supported_subentry_types == {} assert entries[0].data == {"example": "data"} assert len(entries[0].subentries) == 1 subentry_id = list(entries[0].subentries)[0] subentry = config_entries.ConfigSubentry( data=subentrydata["data"], subentry_id=subentry_id, + subentry_type="test", title=subentrydata["title"], unique_id="test", ) @@ -1984,22 +2187,15 @@ async def test_entry_subentry( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2025,11 +2221,66 @@ async def test_entry_subentry( subentry_id: config_entries.ConfigSubentry( data={"second": True}, subentry_id=subentry_id, + subentry_type="test", title="Mock title", unique_id="test", ) } - assert entry.supported_subentries == ("test",) + assert entry.supported_subentry_types == { + "test": {"supports_reconfigure": False} + } + + +async def test_subentry_flow( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test that we can execute a subentry flow.""" + mock_integration(hass, MockModule("test")) + mock_platform(hass, "test.config_flow", None) + entry = MockConfigEntry(domain="test", data={"first": True}) + entry.add_to_manager(manager) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" + + async def async_step_user(self, user_input=None): + return self.async_create_entry( + title="Mock title", + data={"second": True}, + unique_id="test", + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + with mock_config_flow("test", TestFlow): + result = await manager.subentries.async_init( + (entry.entry_id, "test"), context={"source": "user"} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + assert entry.data == {"first": True} + assert entry.options == {} + subentry_id = list(entry.subentries)[0] + assert entry.subentries == { + subentry_id: config_entries.ConfigSubentry( + data={"second": True}, + subentry_id=subentry_id, + subentry_type="test", + title="Mock title", + unique_id="test", + ) + } + assert entry.supported_subentry_types == { + "test": {"supports_reconfigure": False} + } async def test_entry_subentry_non_string( @@ -2044,22 +2295,15 @@ async def test_entry_subentry_non_string( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2093,22 +2337,15 @@ async def test_entry_subentry_no_context( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow), pytest.raises(KeyError): await manager.subentries.async_create_flow( @@ -2136,6 +2373,7 @@ async def test_entry_subentry_duplicate( config_entries.ConfigSubentryData( data={}, subentry_id="blabla", + subentry_type="test", title="Mock title", unique_id=unique_id, ) @@ -2146,22 +2384,15 @@ async def test_entry_subentry_duplicate( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2194,22 +2425,15 @@ async def test_entry_subentry_abort( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2248,22 +2472,15 @@ async def test_entry_subentry_deleted_config_entry( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with mock_config_flow("test", TestFlow): flow = await manager.subentries.async_create_flow( @@ -2286,7 +2503,7 @@ async def test_entry_subentry_deleted_config_entry( ) -async def test_entry_subentry_unsupported( +async def test_entry_subentry_unsupported_subentry_type( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test attempting to start a subentry flow for a config entry without support.""" @@ -2298,22 +2515,15 @@ async def test_entry_subentry_unsupported( class TestFlow(config_entries.ConfigFlow): """Test flow.""" - @staticmethod - @callback - def async_get_subentry_flow(config_entry, subentry_type: str): - """Test subentry flow.""" - - class SubentryFlowHandler(data_entry_flow.FlowHandler): - """Test subentry flow handler.""" - - return SubentryFlowHandler() + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + """Test subentry flow handler.""" @classmethod @callback - def async_supported_subentries( + def async_get_supported_subentry_types( cls, config_entry: ConfigEntry - ) -> tuple[str, ...]: - return ("test",) + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} with ( mock_config_flow("test", TestFlow), @@ -2329,7 +2539,7 @@ async def test_entry_subentry_unsupported( ) -async def test_entry_subentry_unsupported_subentry_type( +async def test_entry_subentry_unsupported( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test attempting to start a subentry flow for a config entry without support.""" @@ -2409,29 +2619,49 @@ async def test_entry_setup_invalid_state( assert entry.state is state -async def test_entry_unload_succeed( - hass: HomeAssistant, manager: config_entries.ConfigEntries +@pytest.mark.parametrize( + ("unload_result", "expected_result", "expected_state", "has_runtime_data"), + [ + (True, True, config_entries.ConfigEntryState.NOT_LOADED, False), + (False, False, config_entries.ConfigEntryState.LOADED, True), + ], +) +async def test_entry_unload( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + unload_result: bool, + expected_result: bool, + expected_state: config_entries.ConfigEntryState, + has_runtime_data: bool, ) -> None: """Test that we can unload an entry.""" - unloads_called = [] + unload_entry_calls = [] - async def verify_runtime_data(*args): + @callback + def verify_runtime_data() -> None: """Verify runtime data.""" assert entry.runtime_data == 2 - unloads_called.append(args) - return True + + async def async_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unload entry.""" + unload_entry_calls.append(None) + verify_runtime_data() + assert entry.state is config_entries.ConfigEntryState.LOADED + return unload_result entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) entry.async_on_unload(verify_runtime_data) entry.runtime_data = 2 - mock_integration(hass, MockModule("comp", async_unload_entry=verify_runtime_data)) + mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) - assert await manager.async_unload(entry.entry_id) - assert len(unloads_called) == 2 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert not hasattr(entry, "runtime_data") + assert await manager.async_unload(entry.entry_id) == expected_result + assert len(unload_entry_calls) == 1 + assert entry.state is expected_state + assert hasattr(entry, "runtime_data") == has_runtime_data @pytest.mark.parametrize( @@ -3239,9 +3469,7 @@ async def test_unique_id_from_discovery_in_setup_retry( VERSION = 1 - async def async_step_dhcp( - self, discovery_info: dhcp.DhcpServiceInfo - ) -> FlowResult: + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Test dhcp step.""" await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() @@ -3277,7 +3505,7 @@ async def test_unique_id_from_discovery_in_setup_retry( discovery_result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( + data=DhcpServiceInfo( hostname="any", ip=host, macaddress=unique_id, @@ -6038,6 +6266,207 @@ async def test_update_entry_and_reload( assert len(comp.async_unload_entry.mock_calls) == calls_entry_load_unload[1] +@pytest.mark.parametrize( + ( + "kwargs", + "expected_title", + "expected_unique_id", + "expected_data", + "raises", + ), + [ + ( + { + "unique_id": "5678", + "title": "Updated title", + "data": {"vendor": "data2"}, + }, + "Updated title", + "5678", + {"vendor": "data2"}, + None, + ), + ( + { + "unique_id": "1234", + "title": "Test", + "data": {"vendor": "data"}, + }, + "Test", + "1234", + {"vendor": "data"}, + None, + ), + ( + {}, + "Test", + "1234", + {"vendor": "data"}, + None, + ), + ( + { + "data": {"buyer": "me"}, + }, + "Test", + "1234", + {"buyer": "me"}, + None, + ), + ( + {"data_updates": {"buyer": "me"}}, + "Test", + "1234", + {"vendor": "data", "buyer": "me"}, + None, + ), + ( + { + "unique_id": "5678", + "title": "Updated title", + "data": {"vendor": "data2"}, + "data_updates": {"buyer": "me"}, + }, + "Test", + "1234", + {"vendor": "data"}, + ValueError, + ), + ], + ids=[ + "changed_entry_default", + "unchanged_entry_default", + "no_kwargs", + "replace_data", + "update_data", + "update_and_data_raises", + ], +) +async def test_update_subentry_and_abort( + hass: HomeAssistant, + expected_title: str, + expected_unique_id: str, + expected_data: dict[str, Any], + kwargs: dict[str, Any], + raises: type[Exception] | None, +) -> None: + """Test updating an entry and reloading.""" + subentry_id = "blabla" + entry = MockConfigEntry( + domain="comp", + unique_id="entry_unique_id", + title="entry_title", + data={}, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="1234", + title="Test", + ) + ], + ) + entry.add_to_hass(hass) + subentry = entry.subentries[subentry_id] + + comp = MockModule("comp") + mock_integration(hass, comp) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + async def async_step_reconfigure(self, user_input=None): + return self.async_update_and_abort( + self._get_reconfigure_entry(), + self._get_reconfigure_subentry(), + **kwargs, + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: config_entries.ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + err: Exception + with mock_config_flow("comp", TestFlow): + try: + result = await entry.start_subentry_reconfigure_flow( + hass, "test", subentry_id + ) + except Exception as ex: # noqa: BLE001 + err = ex + + await hass.async_block_till_done() + + subentry = entry.subentries[subentry_id] + assert subentry.title == expected_title + assert subentry.unique_id == expected_unique_id + assert subentry.data == expected_data + if raises: + assert isinstance(err, raises) + else: + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +async def test_reconfigure_subentry_create_subentry(hass: HomeAssistant) -> None: + """Test it's not allowed to create a subentry from a subentry reconfigure flow.""" + subentry_id = "blabla" + entry = MockConfigEntry( + domain="comp", + unique_id="entry_unique_id", + title="entry_title", + data={}, + subentries_data=[ + config_entries.ConfigSubentryData( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="1234", + title="Test", + ) + ], + ) + entry.add_to_hass(hass) + + comp = MockModule("comp") + mock_integration(hass, comp) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + async def async_step_reconfigure(self, user_input=None): + return self.async_create_entry(title="New Subentry", data={}) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: config_entries.ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + with ( + mock_config_flow("comp", TestFlow), + pytest.raises(ValueError, match="Source is reconfigure, expected user"), + ): + await entry.start_subentry_reconfigure_flow(hass, "test", subentry_id) + + await hass.async_block_till_done() + + assert entry.subentries == { + subentry_id: config_entries.ConfigSubentry( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + title="Test", + unique_id="1234", + ) + } + + @pytest.mark.parametrize("unique_id", [["blah", "bleh"], {"key": "value"}]) async def test_unhashable_unique_id_fails( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, unique_id: Any @@ -6581,6 +7010,23 @@ async def test_updating_non_added_entry_raises(hass: HomeAssistant) -> None: hass.config_entries.async_update_entry(entry, unique_id="new_id") +async def test_updating_non_added_subentry_raises(hass: HomeAssistant) -> None: + """Test updating a non added entry raises UnknownEntry.""" + entry = MockConfigEntry(domain="test") + subentry = config_entries.ConfigSubentry( + data={}, + subentry_type="test", + title="Mock title", + unique_id="unique", + ) + + with pytest.raises(config_entries.UnknownEntry, match=entry.entry_id): + hass.config_entries.async_update_subentry(entry, subentry, unique_id="new_id") + entry.add_to_hass(hass) + with pytest.raises(config_entries.UnknownSubEntry, match=subentry.subentry_id): + hass.config_entries.async_update_subentry(entry, subentry, unique_id="new_id") + + async def test_reload_during_setup(hass: HomeAssistant) -> None: """Test reload during setup waits.""" entry = MockConfigEntry(domain="comp", data={"value": "initial"}) @@ -6662,7 +7108,7 @@ async def test_raise_wrong_exception_in_forwarded_platform( async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" raise exc @@ -6736,7 +7182,7 @@ async def test_config_entry_unloaded_during_platform_setups( async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" await asyncio.sleep(0) @@ -6808,7 +7254,7 @@ async def test_non_awaited_async_forward_entry_setups( async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" await forward_event.wait() @@ -6880,7 +7326,7 @@ async def test_non_awaited_async_forward_entry_setup( async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" await forward_event.wait() @@ -6955,7 +7401,7 @@ async def test_config_entry_unloaded_during_platform_setup( async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" await asyncio.sleep(0) @@ -7030,7 +7476,7 @@ async def test_config_entry_late_platform_setup( async def mock_setup_entry_platform( hass: HomeAssistant, entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, + async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Mock setting up platform.""" await asyncio.sleep(0) @@ -7449,7 +7895,7 @@ async def test_get_reauth_entry( async def test_get_reconfigure_entry( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: - """Test _get_context_entry behavior.""" + """Test _get_reconfigure_entry behavior.""" entry = MockConfigEntry( title="test_title", domain="test", @@ -7524,6 +7970,114 @@ async def test_get_reconfigure_entry( assert result["reason"] == "Source is user, expected reconfigure: -" +async def test_subentry_get_reconfigure_entry( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test subentry _get_reconfigure_entry and _get_reconfigure_subentry behavior.""" + subentry_id = "mock_subentry_id" + entry = MockConfigEntry( + data={}, + domain="test", + entry_id="mock_entry_id", + title="entry_title", + unique_id="entry_unique_id", + subentries_data=[ + config_entries.ConfigSubentryData( + data={"vendor": "data"}, + subentry_id=subentry_id, + subentry_type="test", + unique_id="1234", + title="Test", + ) + ], + ) + + entry.add_to_hass(hass) + + mock_integration(hass, MockModule("test")) + mock_platform(hass, "test.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + class SubentryFlowHandler(config_entries.ConfigSubentryFlow): + async def async_step_user(self, user_input=None): + """Test user step.""" + return await self._async_step_confirm() + + async def async_step_reconfigure(self, user_input=None): + """Test reauth step.""" + return await self._async_step_confirm() + + async def _async_step_confirm(self): + """Confirm input.""" + try: + entry = self._get_reconfigure_entry() + except ValueError as err: + reason = str(err) + else: + reason = f"Found entry {entry.title}" + try: + entry_id = self._reconfigure_entry_id + except ValueError: + reason = f"{reason}: -" + else: + reason = f"{reason}: {entry_id}" + + try: + subentry = self._get_reconfigure_subentry() + except ValueError as err: + reason = f"{reason}/{err}" + except config_entries.UnknownSubEntry: + reason = f"{reason}/Subentry not found" + else: + reason = f"{reason}/Found subentry {subentry.title}" + try: + subentry_id = self._reconfigure_subentry_id + except ValueError: + reason = f"{reason}: -" + else: + reason = f"{reason}: {subentry_id}" + return self.async_abort(reason=reason) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: config_entries.ConfigEntry + ) -> dict[str, type[config_entries.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + # A reconfigure flow finds the config entry + with mock_config_flow("test", TestFlow): + result = await entry.start_subentry_reconfigure_flow(hass, "test", subentry_id) + assert ( + result["reason"] + == "Found entry entry_title: mock_entry_id/Found subentry Test: mock_subentry_id" + ) + + # The subentry_id does not exist + with mock_config_flow("test", TestFlow): + result = await manager.subentries.async_init( + (entry.entry_id, "test"), + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "subentry_id": "01JRemoved", + }, + ) + assert ( + result["reason"] + == "Found entry entry_title: mock_entry_id/Subentry not found: 01JRemoved" + ) + + # A user flow does not have access to the config entry or subentry + with mock_config_flow("test", TestFlow): + result = await manager.subentries.async_init( + (entry.entry_id, "test"), context={"source": config_entries.SOURCE_USER} + ) + assert ( + result["reason"] + == "Source is user, expected reconfigure: -/Source is user, expected reconfigure: -" + ) + + async def test_reauth_helper_alignment( hass: HomeAssistant, manager: config_entries.ConfigEntries, @@ -7865,9 +8419,9 @@ async def test_unique_id_collision_issues( mock_setup_entry = AsyncMock(return_value=True) for i in range(3): mock_integration( - hass, MockModule(f"test{i+1}", async_setup_entry=mock_setup_entry) + hass, MockModule(f"test{i + 1}", async_setup_entry=mock_setup_entry) ) - mock_platform(hass, f"test{i+1}.config_flow", None) + mock_platform(hass, f"test{i + 1}.config_flow", None) test2_group_1: list[MockConfigEntry] = [] test2_group_2: list[MockConfigEntry] = [] diff --git a/tests/test_core.py b/tests/test_core.py index 60b907d57ca..ceab3ce327c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -20,6 +20,7 @@ import pytest from pytest_unordered import unordered import voluptuous as vol +from homeassistant import core as ha from homeassistant.const import ( ATTR_FRIENDLY_NAME, EVENT_CALL_SERVICE, @@ -35,7 +36,6 @@ from homeassistant.const import ( EVENT_STATE_REPORTED, MATCH_ALL, ) -import homeassistant.core as ha from homeassistant.core import ( CoreState, HassJob, @@ -59,8 +59,8 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers.json import json_dumps from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.async_ import create_eager_task -import homeassistant.util.dt as dt_util from homeassistant.util.read_only_dict import ReadOnlyDict from .common import ( diff --git a/tests/test_core_config.py b/tests/test_core_config.py index dae50bae097..2723c8e7196 100644 --- a/tests/test_core_config.py +++ b/tests/test_core_config.py @@ -46,7 +46,7 @@ from homeassistant.util.unit_system import ( UnitSystem, ) -from .common import MockUser, async_capture_events +from .common import MockEntityPlatform, MockUser, async_capture_events def test_core_config_schema() -> None: @@ -181,7 +181,8 @@ def test_validate_stun_or_turn_url() -> None: invalid_urls = ( "custom_stun_server", "custom_stun_server:3478", - "bum:custom_stun_server:3478" "http://blah.com:80", + "bum:custom_stun_server:3478", + "http://blah.com:80", ) valid_urls = ( @@ -221,6 +222,7 @@ async def _compute_state(hass: HomeAssistant, config: dict[str, Any]) -> State | entity = Entity() entity.entity_id = "test.test" entity.hass = hass + entity.platform = MockEntityPlatform(hass) entity.schedule_update_ha_state() await hass.async_block_till_done() diff --git a/tests/test_setup.py b/tests/test_setup.py index 2d15c670cf7..bb221c7cb4c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -363,20 +363,24 @@ async def test_component_failing_setup(hass: HomeAssistant) -> None: async def test_component_exception_setup(hass: HomeAssistant) -> None: """Test component that raises exception during setup.""" - setup.async_set_domains_to_be_loaded(hass, {"comp"}) + domain = "comp" + setup.async_set_domains_to_be_loaded(hass, {domain}) def exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Raise exception.""" raise Exception("fail!") # noqa: TRY002 - mock_integration(hass, MockModule("comp", setup=exception_setup)) + mock_integration(hass, MockModule(domain, setup=exception_setup)) - assert not await setup.async_setup_component(hass, "comp", {}) - assert "comp" not in hass.config.components + assert not await setup.async_setup_component(hass, domain, {}) + assert domain in hass.data[setup.DATA_SETUP] + assert domain not in hass.data[setup.DATA_SETUP_DONE] + assert domain not in hass.config.components async def test_component_base_exception_setup(hass: HomeAssistant) -> None: """Test component that raises exception during setup.""" + domain = "comp" setup.async_set_domains_to_be_loaded(hass, {"comp"}) def exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -389,7 +393,69 @@ async def test_component_base_exception_setup(hass: HomeAssistant) -> None: await setup.async_setup_component(hass, "comp", {}) assert str(exc_info.value) == "fail!" - assert "comp" not in hass.config.components + assert domain in hass.data[setup.DATA_SETUP] + assert domain not in hass.data[setup.DATA_SETUP_DONE] + assert domain not in hass.config.components + + +async def test_set_domains_to_be_loaded(hass: HomeAssistant) -> None: + """Test async_set_domains_to_be_loaded.""" + domain_good = "comp_good" + domain_bad = "comp_bad" + domain_base_exception = "comp_base_exception" + domain_exception = "comp_exception" + domains = {domain_good, domain_bad, domain_exception, domain_base_exception} + setup.async_set_domains_to_be_loaded(hass, domains) + + assert set(hass.data[setup.DATA_SETUP_DONE]) == domains + setup_done = dict(hass.data[setup.DATA_SETUP_DONE]) + + # Calling async_set_domains_to_be_loaded again should not create new futures + setup.async_set_domains_to_be_loaded(hass, domains) + assert setup_done == hass.data[setup.DATA_SETUP_DONE] + + def good_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Success.""" + return True + + def bad_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Fail.""" + return False + + def base_exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Raise exception.""" + raise BaseException("fail!") # noqa: TRY002 + + def exception_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Raise exception.""" + raise Exception("fail!") # noqa: TRY002 + + mock_integration(hass, MockModule(domain_good, setup=good_setup)) + mock_integration(hass, MockModule(domain_bad, setup=bad_setup)) + mock_integration( + hass, MockModule(domain_base_exception, setup=base_exception_setup) + ) + mock_integration(hass, MockModule(domain_exception, setup=exception_setup)) + + # Set up the four components + assert await setup.async_setup_component(hass, domain_good, {}) + assert not await setup.async_setup_component(hass, domain_bad, {}) + assert not await setup.async_setup_component(hass, domain_exception, {}) + with pytest.raises(BaseException, match="fail!"): + await setup.async_setup_component(hass, domain_base_exception, {}) + + # Check the result of the setup + assert not hass.data[setup.DATA_SETUP_DONE] + assert set(hass.data[setup.DATA_SETUP]) == { + domain_bad, + domain_exception, + domain_base_exception, + } + assert set(hass.config.components) == {domain_good} + + # Calling async_set_domains_to_be_loaded again should not create any new futures + setup.async_set_domains_to_be_loaded(hass, domains) + assert not hass.data[setup.DATA_SETUP_DONE] async def test_component_setup_with_validation_and_dependency( diff --git a/tests/typing.py b/tests/typing.py index 7b61949a9c4..5bcb1a01104 100644 --- a/tests/typing.py +++ b/tests/typing.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine +from contextlib import AbstractAsyncContextManager from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock @@ -30,6 +31,10 @@ type MqttMockHAClient = MagicMock """MagicMock for `homeassistant.components.mqtt.MQTT`.""" type MqttMockHAClientGenerator = Callable[..., Coroutine[Any, Any, MqttMockHAClient]] """MagicMock generator for `homeassistant.components.mqtt.MQTT`.""" +type RecorderInstanceContextManager = Callable[ + ..., AbstractAsyncContextManager[Recorder] +] +"""ContextManager for `homeassistant.components.recorder.Recorder`.""" type RecorderInstanceGenerator = Callable[..., Coroutine[Any, Any, Recorder]] """Instance generator for `homeassistant.components.recorder.Recorder`.""" type WebSocketGenerator = Callable[..., Coroutine[Any, Any, MockHAClientWebSocket]] diff --git a/tests/util/test_async.py b/tests/util/test_async.py index cfa78228f0c..e2310e6acd5 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -134,8 +134,10 @@ async def test_create_eager_task_312(hass: HomeAssistant) -> None: async def test_create_eager_task_from_thread(hass: HomeAssistant) -> None: """Test we report trying to create an eager task from a thread.""" + coro = asyncio.sleep(0) + def create_task(): - hasync.create_eager_task(asyncio.sleep(0)) + hasync.create_eager_task(coro) with pytest.raises( RuntimeError, @@ -145,14 +147,19 @@ async def test_create_eager_task_from_thread(hass: HomeAssistant) -> None: ): await hass.async_add_executor_job(create_task) + # Avoid `RuntimeWarning: coroutine 'sleep' was never awaited` + await coro + async def test_create_eager_task_from_thread_in_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test we report trying to create an eager task from a thread.""" + coro = asyncio.sleep(0) + def create_task(): - hasync.create_eager_task(asyncio.sleep(0)) + hasync.create_eager_task(coro) frames = extract_stack_to_frame( [ @@ -200,6 +207,9 @@ async def test_create_eager_task_from_thread_in_integration( "self.light.is_on" ) in caplog.text + # Avoid `RuntimeWarning: coroutine 'sleep' was never awaited` + await coro + async def test_get_scheduled_timer_handles(hass: HomeAssistant) -> None: """Test get_scheduled_timer_handles returns all scheduled timer handles.""" diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 165552b8792..bcaf392b513 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -6,7 +6,7 @@ import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol -import homeassistant.util.color as color_util +from homeassistant.util import color as color_util GAMUT = color_util.GamutType( color_util.XYPoint(0.704, 0.296), diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 347e92d6056..96ba8d0a325 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -6,7 +6,7 @@ from datetime import UTC, datetime, timedelta import pytest -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util DEFAULT_TIME_ZONE = dt_util.get_default_time_zone() TEST_TIME_ZONE = "America/Los_Angeles" diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 759f0d6e5ea..111b086b48b 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant import util -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util def test_raise_if_invalid_filename() -> None: diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 3af3ad2765a..ecb54eeeaa9 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -7,7 +7,7 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.util.location as location_util +from homeassistant.util import location as location_util from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 795444c89bd..e5b85f35693 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -14,7 +14,7 @@ from homeassistant.core import ( is_callback, is_callback_check_partial, ) -import homeassistant.util.logging as logging_util +from homeassistant.util import logging as logging_util async def test_logging_with_queue_handler() -> None: diff --git a/tests/util/test_network.py b/tests/util/test_network.py index 4bb6f94e684..c234a517640 100644 --- a/tests/util/test_network.py +++ b/tests/util/test_network.py @@ -2,7 +2,7 @@ from ipaddress import ip_address -import homeassistant.util.network as network_util +from homeassistant.util import network as network_util def test_is_loopback() -> None: diff --git a/tests/util/test_package.py b/tests/util/test_package.py index b7497d620cd..e3635dd2bea 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -410,3 +410,47 @@ def test_check_package_previous_failed_install() -> None: with patch("homeassistant.util.package.version", return_value=None): assert not package.is_installed(installed_package) assert not package.is_installed(f"{installed_package}=={installed_version}") + + +@pytest.mark.parametrize("dockerenv", [True, False], ids=["dockerenv", "not_dockerenv"]) +@pytest.mark.parametrize( + "containerenv", [True, False], ids=["containerenv", "not_containerenv"] +) +@pytest.mark.parametrize( + "kubernetes_service_host", [True, False], ids=["kubernetes", "not_kubernetes"] +) +@pytest.mark.parametrize( + "is_official_image", [True, False], ids=["official_image", "not_official_image"] +) +async def test_is_docker_env( + dockerenv: bool, + containerenv: bool, + kubernetes_service_host: bool, + is_official_image: bool, +) -> None: + """Test is_docker_env.""" + + def new_path_mock(path: str): + mock = Mock() + if path == "/.dockerenv": + mock.exists.return_value = dockerenv + elif path == "/run/.containerenv": + mock.exists.return_value = containerenv + return mock + + env = {} + if kubernetes_service_host: + env["KUBERNETES_SERVICE_HOST"] = "True" + + package.is_docker_env.cache_clear() + with ( + patch("homeassistant.util.package.Path", side_effect=new_path_mock), + patch( + "homeassistant.util.package.is_official_image", + return_value=is_official_image, + ), + patch.dict(os.environ, env), + ): + assert package.is_docker_env() is any( + [dockerenv, containerenv, kubernetes_service_host, is_official_image] + ) diff --git a/tests/util/test_system_info.py b/tests/util/test_system_info.py new file mode 100644 index 00000000000..270e91d37db --- /dev/null +++ b/tests/util/test_system_info.py @@ -0,0 +1,15 @@ +"""Tests for the system info helper.""" + +from unittest.mock import patch + +from homeassistant.util.system_info import is_official_image + + +async def test_is_official_image() -> None: + """Test is_official_image.""" + is_official_image.cache_clear() + with patch("homeassistant.util.system_info.os.path.isfile", return_value=True): + assert is_official_image() is True + is_official_image.cache_clear() + with patch("homeassistant.util.system_info.os.path.isfile", return_value=False): + assert is_official_image() is False diff --git a/tests/util/test_ulid.py b/tests/util/test_ulid.py index dc0f21ce3c7..6f9911fe557 100644 --- a/tests/util/test_ulid.py +++ b/tests/util/test_ulid.py @@ -2,7 +2,7 @@ import uuid -import homeassistant.util.ulid as ulid_util +from homeassistant.util import ulid as ulid_util async def test_ulid_util_uuid_hex() -> None: diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 4be32b2851e..aeea4ad9a5a 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -18,6 +18,7 @@ from homeassistant.const import ( UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, + UnitOfEnergyDistance, UnitOfInformation, UnitOfLength, UnitOfMass, @@ -43,6 +44,7 @@ from homeassistant.util.unit_conversion import ( ElectricCurrentConverter, ElectricPotentialConverter, EnergyConverter, + EnergyDistanceConverter, InformationConverter, MassConverter, PowerConverter, @@ -79,6 +81,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { SpeedConverter, TemperatureConverter, UnitlessRatioConverter, + EnergyDistanceConverter, VolumeConverter, VolumeFlowRateConverter, ) @@ -115,6 +118,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo 1000, ), EnergyConverter: (UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), + EnergyDistanceConverter: ( + UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR, + UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR, + 0.621371, + ), InformationConverter: (UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473), PowerConverter: (UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), @@ -435,10 +443,24 @@ _CONVERTED_VALUE: dict[ ElectricPotentialConverter: [ (5, UnitOfElectricPotential.VOLT, 5000, UnitOfElectricPotential.MILLIVOLT), (5, UnitOfElectricPotential.VOLT, 5e6, UnitOfElectricPotential.MICROVOLT), + (5, UnitOfElectricPotential.VOLT, 5e-3, UnitOfElectricPotential.KILOVOLT), + (5, UnitOfElectricPotential.VOLT, 5e-6, UnitOfElectricPotential.MEGAVOLT), (5, UnitOfElectricPotential.MILLIVOLT, 0.005, UnitOfElectricPotential.VOLT), (5, UnitOfElectricPotential.MILLIVOLT, 5e3, UnitOfElectricPotential.MICROVOLT), + (5, UnitOfElectricPotential.MILLIVOLT, 5e-6, UnitOfElectricPotential.KILOVOLT), + (5, UnitOfElectricPotential.MILLIVOLT, 5e-9, UnitOfElectricPotential.MEGAVOLT), (5, UnitOfElectricPotential.MICROVOLT, 5e-3, UnitOfElectricPotential.MILLIVOLT), (5, UnitOfElectricPotential.MICROVOLT, 5e-6, UnitOfElectricPotential.VOLT), + (5, UnitOfElectricPotential.MICROVOLT, 5e-9, UnitOfElectricPotential.KILOVOLT), + (5, UnitOfElectricPotential.MICROVOLT, 5e-12, UnitOfElectricPotential.MEGAVOLT), + (5, UnitOfElectricPotential.KILOVOLT, 5e9, UnitOfElectricPotential.MICROVOLT), + (5, UnitOfElectricPotential.KILOVOLT, 5e6, UnitOfElectricPotential.MILLIVOLT), + (5, UnitOfElectricPotential.KILOVOLT, 5e3, UnitOfElectricPotential.VOLT), + (5, UnitOfElectricPotential.KILOVOLT, 5e-3, UnitOfElectricPotential.MEGAVOLT), + (5, UnitOfElectricPotential.MEGAVOLT, 5e12, UnitOfElectricPotential.MICROVOLT), + (5, UnitOfElectricPotential.MEGAVOLT, 5e9, UnitOfElectricPotential.MILLIVOLT), + (5, UnitOfElectricPotential.MEGAVOLT, 5e6, UnitOfElectricPotential.VOLT), + (5, UnitOfElectricPotential.MEGAVOLT, 5e3, UnitOfElectricPotential.KILOVOLT), ], EnergyConverter: [ (10, UnitOfEnergy.MILLIWATT_HOUR, 0.00001, UnitOfEnergy.KILO_WATT_HOUR), @@ -472,6 +494,38 @@ _CONVERTED_VALUE: dict[ (10, UnitOfEnergy.GIGA_CALORIE, 10000, UnitOfEnergy.MEGA_CALORIE), (10, UnitOfEnergy.GIGA_CALORIE, 11.622222, UnitOfEnergy.MEGA_WATT_HOUR), ], + EnergyDistanceConverter: [ + ( + 10, + UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, + 6.213712, + UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR, + ), + ( + 25, + UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, + 4, + UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR, + ), + ( + 20, + UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR, + 3.106856, + UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, + ), + ( + 10, + UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR, + 16.09344, + UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR, + ), + ( + 16.09344, + UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR, + 10, + UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR, + ), + ], InformationConverter: [ (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS), (8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS), @@ -537,6 +591,7 @@ _CONVERTED_VALUE: dict[ (10, UnitOfPower.GIGA_WATT, 10e9, UnitOfPower.WATT), (10, UnitOfPower.TERA_WATT, 10e12, UnitOfPower.WATT), (10, UnitOfPower.WATT, 0.01, UnitOfPower.KILO_WATT), + (10, UnitOfPower.MILLIWATT, 0.01, UnitOfPower.WATT), ], PressureConverter: [ (1000, UnitOfPressure.HPA, 14.5037743897, UnitOfPressure.PSI), diff --git a/tests/util/test_uuid.py b/tests/util/test_uuid.py index e5a1022ef1d..9d78b634149 100644 --- a/tests/util/test_uuid.py +++ b/tests/util/test_uuid.py @@ -2,7 +2,7 @@ import uuid -import homeassistant.util.uuid as uuid_util +from homeassistant.util import uuid as uuid_util async def test_uuid_util_random_uuid_hex() -> None: diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 12a7eca5f9d..0346e21044f 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -15,7 +15,7 @@ import yaml as pyyaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import yaml +from homeassistant.util import yaml as yaml_util from homeassistant.util.yaml import loader as yaml_loader from tests.common import extract_stack_to_frame @@ -86,7 +86,7 @@ def test_unhashable_key() -> None: def test_no_key() -> None: """Test item without a key.""" with pytest.raises(HomeAssistantError): - yaml.load_yaml(YAML_CONFIG_FILE) + yaml_util.load_yaml(YAML_CONFIG_FILE) @pytest.mark.usefixtures("try_both_loaders") @@ -386,13 +386,13 @@ def test_load_yaml_encoding_error(mock_open: Mock) -> None: @pytest.mark.usefixtures("try_both_dumpers") def test_dump() -> None: """The that the dump method returns empty None values.""" - assert yaml.dump({"a": None, "b": "b"}) == "a:\nb: b\n" + assert yaml_util.dump({"a": None, "b": "b"}) == "a:\nb: b\n" @pytest.mark.usefixtures("try_both_dumpers") def test_dump_unicode() -> None: """The that the dump method returns empty None values.""" - assert yaml.dump({"a": None, "b": "привет"}) == "a:\nb: привет\n" + assert yaml_util.dump({"a": None, "b": "привет"}) == "a:\nb: привет\n" @pytest.mark.parametrize("hass_config_yaml", ['key: [1, "2", 3]']) @@ -400,7 +400,7 @@ def test_dump_unicode() -> None: def test_representing_yaml_loaded_data() -> None: """Test we can represent YAML loaded data.""" data = load_yaml_config_file(YAML_CONFIG_FILE) - assert yaml.dump(data) == "key:\n- 1\n- '2'\n- 3\n" + assert yaml_util.dump(data) == "key:\n- 1\n- '2'\n- 3\n" @pytest.mark.parametrize("hass_config_yaml", ["key: thing1\nkey: thing2"]) @@ -413,7 +413,7 @@ def test_duplicate_key(caplog: pytest.LogCaptureFixture) -> None: @pytest.mark.parametrize( "hass_config_yaml_files", - [{YAML_CONFIG_FILE: "key: !secret a", yaml.SECRET_YAML: "a: 1\nb: !secret a"}], + [{YAML_CONFIG_FILE: "key: !secret a", yaml_util.SECRET_YAML: "a: 1\nb: !secret a"}], ) @pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") def test_no_recursive_secrets() -> None: @@ -426,8 +426,8 @@ def test_no_recursive_secrets() -> None: def test_input_class() -> None: """Test input class.""" - yaml_input = yaml.Input("hello") - yaml_input2 = yaml.Input("hello") + yaml_input = yaml_util.Input("hello") + yaml_input2 = yaml_util.Input("hello") assert yaml_input.name == "hello" assert yaml_input == yaml_input2 @@ -438,8 +438,8 @@ def test_input_class() -> None: @pytest.mark.usefixtures("try_both_loaders", "try_both_dumpers") def test_input() -> None: """Test loading inputs.""" - data = {"hello": yaml.Input("test_name")} - assert yaml.parse_yaml(yaml.dump(data)) == data + data = {"hello": yaml_util.Input("test_name")} + assert yaml_util.parse_yaml(yaml_util.dump(data)) == data @pytest.mark.skipif( @@ -448,7 +448,7 @@ def test_input() -> None: ) def test_c_loader_is_available_in_ci() -> None: """Verify we are testing the C loader in the CI.""" - assert yaml.loader.HAS_C_LOADER is True + assert yaml_util.loader.HAS_C_LOADER is True @pytest.mark.usefixtures("try_both_loaders") @@ -552,7 +552,7 @@ def test_string_used_as_vol_schema() -> None: @pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") def test_load_yaml_dict(expected_data: Any) -> None: """Test item without a key.""" - assert yaml.load_yaml_dict(YAML_CONFIG_FILE) == expected_data + assert yaml_util.load_yaml_dict(YAML_CONFIG_FILE) == expected_data @pytest.mark.parametrize("hass_config_yaml", ["abc", "123", "[]"]) diff --git a/tests/util/yaml/test_secrets.py b/tests/util/yaml/test_secrets.py index 35b5ae319c4..4d89bfb8712 100644 --- a/tests/util/yaml/test_secrets.py +++ b/tests/util/yaml/test_secrets.py @@ -8,7 +8,7 @@ import pytest from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import yaml +from homeassistant.util import yaml as yaml_util from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_config_dir, patch_yaml_files @@ -63,7 +63,7 @@ def default_config(filepaths: dict[str, Path]) -> YamlFile: def default_secrets(filepaths: dict[str, Path]) -> YamlFile: """Return the default secrets file for testing.""" return YamlFile( - path=filepaths["config"] / yaml.SECRET_YAML, + path=filepaths["config"] / yaml_util.SECRET_YAML, contents=( "http_pw: pwhttp\n" "comp1_un: un1\n" @@ -112,7 +112,8 @@ def test_secret_overrides_parent( path=filepaths["sub_folder"] / "sub.yaml", contents=default_config.contents ) sub_secrets = YamlFile( - path=filepaths["sub_folder"] / yaml.SECRET_YAML, contents="http_pw: override" + path=filepaths["sub_folder"] / yaml_util.SECRET_YAML, + contents="http_pw: override", ) loaded_file = load_config_file( @@ -133,7 +134,7 @@ def test_secrets_from_unrelated_fails( contents="http:\n api_password: !secret test", ) unrelated_secrets = YamlFile( - path=filepaths["unrelated"] / yaml.SECRET_YAML, contents="test: failure" + path=filepaths["unrelated"] / yaml_util.SECRET_YAML, contents="test: failure" ) with pytest.raises(HomeAssistantError, match="Secret test not defined"): load_config_file( @@ -162,7 +163,8 @@ def test_bad_logger_value( path=filepaths["config"] / YAML_CONFIG_FILE, contents="api_password: !secret pw" ) secrets_file = YamlFile( - path=filepaths["config"] / yaml.SECRET_YAML, contents="logger: info\npw: abc" + path=filepaths["config"] / yaml_util.SECRET_YAML, + contents="logger: info\npw: abc", ) with caplog.at_level(logging.ERROR): load_config_file(config_file.path, [config_file, secrets_file]) @@ -178,7 +180,7 @@ def test_secrets_are_not_dict( ) -> None: """Did secrets handle non-dict file.""" non_dict_secrets = YamlFile( - path=filepaths["config"] / yaml.SECRET_YAML, + path=filepaths["config"] / yaml_util.SECRET_YAML, contents="- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n", ) with pytest.raises(HomeAssistantError, match="Secrets is not a dictionary"):