diff --git a/.coveragerc b/.coveragerc index e682e1741e8..b6940d4ff08 100644 --- a/.coveragerc +++ b/.coveragerc @@ -745,11 +745,7 @@ omit = homeassistant/components/mikrotik/hub.py homeassistant/components/mill/climate.py homeassistant/components/mill/sensor.py - homeassistant/components/minecraft_server/api.py homeassistant/components/minecraft_server/binary_sensor.py - homeassistant/components/minecraft_server/coordinator.py - homeassistant/components/minecraft_server/entity.py - homeassistant/components/minecraft_server/sensor.py homeassistant/components/minio/minio_helper.py homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py diff --git a/tests/components/minecraft_server/snapshots/test_sensor.ambr b/tests/components/minecraft_server/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fed0ae93c66 --- /dev/null +++ b/tests/components/minecraft_server/snapshots/test_sensor.ambr @@ -0,0 +1,413 @@ +# serializer version: 1 +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Latency', + 'icon': 'mdi:signal', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_latency', + 'last_changed': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players online', + 'icon': 'mdi:account-multiple', + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_online', + 'last_changed': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players max', + 'icon': 'mdi:account-multiple', + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_max', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server World message', + 'icon': 'mdi:minecraft', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_world_message', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy MOTD', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_version', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Version', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Protocol version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_protocol_version', + 'last_changed': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Map name', + 'icon': 'mdi:map', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_map_name', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Map Name', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Game mode', + 'icon': 'mdi:cog', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_game_mode', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Game Mode', + }) +# --- +# name: test_sensor[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Edition', + 'icon': 'mdi:minecraft', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_edition', + 'last_changed': , + 'last_updated': , + 'state': 'MCPE', + }) +# --- +# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Latency', + 'icon': 'mdi:signal', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_latency', + 'last_changed': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players online', + 'icon': 'mdi:account-multiple', + 'players_list': list([ + 'Player 1', + 'Player 2', + 'Player 3', + ]), + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_online', + 'last_changed': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players max', + 'icon': 'mdi:account-multiple', + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_max', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server World message', + 'icon': 'mdi:minecraft', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_world_message', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy MOTD', + }) +# --- +# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_version', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Version', + }) +# --- +# name: test_sensor[java_mock_config_entry-JavaServer-status_response0-entity_ids0].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Protocol version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_protocol_version', + 'last_changed': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Latency', + 'icon': 'mdi:signal', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_latency', + 'last_changed': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players online', + 'icon': 'mdi:account-multiple', + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_online', + 'last_changed': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players max', + 'icon': 'mdi:account-multiple', + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_max', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server World message', + 'icon': 'mdi:minecraft', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_world_message', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy MOTD', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_version', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Version', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Protocol version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_protocol_version', + 'last_changed': , + 'last_updated': , + 'state': '123', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].6 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Map name', + 'icon': 'mdi:map', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_map_name', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Map Name', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].7 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Game mode', + 'icon': 'mdi:cog', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_game_mode', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Game Mode', + }) +# --- +# name: test_sensor_update[bedrock_mock_config_entry-BedrockServer-status_response1-entity_ids1].8 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Edition', + 'icon': 'mdi:minecraft', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_edition', + 'last_changed': , + 'last_updated': , + 'state': 'MCPE', + }) +# --- +# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Latency', + 'icon': 'mdi:signal', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_latency', + 'last_changed': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players online', + 'icon': 'mdi:account-multiple', + 'players_list': list([ + 'Player 1', + 'Player 2', + 'Player 3', + ]), + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_online', + 'last_changed': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Players max', + 'icon': 'mdi:account-multiple', + 'unit_of_measurement': 'players', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_players_max', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].3 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server World message', + 'icon': 'mdi:minecraft', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_world_message', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy MOTD', + }) +# --- +# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].4 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_version', + 'last_changed': , + 'last_updated': , + 'state': 'Dummy Version', + }) +# --- +# name: test_sensor_update[java_mock_config_entry-JavaServer-status_response0-entity_ids0].5 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Minecraft Server Protocol version', + 'icon': 'mdi:numeric', + }), + 'context': , + 'entity_id': 'sensor.minecraft_server_protocol_version', + 'last_changed': , + 'last_updated': , + 'state': '123', + }) +# --- diff --git a/tests/components/minecraft_server/test_sensor.py b/tests/components/minecraft_server/test_sensor.py new file mode 100644 index 00000000000..006c735e034 --- /dev/null +++ b/tests/components/minecraft_server/test_sensor.py @@ -0,0 +1,239 @@ +"""Tests for Minecraft Server sensors.""" +from datetime import timedelta +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +from mcstatus import BedrockServer, JavaServer +from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from .const import ( + TEST_BEDROCK_STATUS_RESPONSE, + TEST_HOST, + TEST_JAVA_STATUS_RESPONSE, + TEST_PORT, +) + +from tests.common import async_fire_time_changed + +JAVA_SENSOR_ENTITIES: list[str] = [ + "sensor.minecraft_server_latency", + "sensor.minecraft_server_players_online", + "sensor.minecraft_server_players_max", + "sensor.minecraft_server_world_message", + "sensor.minecraft_server_version", + "sensor.minecraft_server_protocol_version", +] + +JAVA_SENSOR_ENTITIES_DISABLED_BY_DEFAULT: list[str] = [ + "sensor.minecraft_server_players_max", + "sensor.minecraft_server_protocol_version", +] + +BEDROCK_SENSOR_ENTITIES: list[str] = [ + "sensor.minecraft_server_latency", + "sensor.minecraft_server_players_online", + "sensor.minecraft_server_players_max", + "sensor.minecraft_server_world_message", + "sensor.minecraft_server_version", + "sensor.minecraft_server_protocol_version", + "sensor.minecraft_server_map_name", + "sensor.minecraft_server_game_mode", + "sensor.minecraft_server_edition", +] + +BEDROCK_SENSOR_ENTITIES_DISABLED_BY_DEFAULT: list[str] = [ + "sensor.minecraft_server_players_max", + "sensor.minecraft_server_protocol_version", + "sensor.minecraft_server_edition", +] + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("mock_config_entry", "server", "status_response", "entity_ids"), + [ + ( + "java_mock_config_entry", + JavaServer, + TEST_JAVA_STATUS_RESPONSE, + JAVA_SENSOR_ENTITIES, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + TEST_BEDROCK_STATUS_RESPONSE, + BEDROCK_SENSOR_ENTITIES, + ), + ], +) +async def test_sensor( + hass: HomeAssistant, + mock_config_entry: str, + server: JavaServer | BedrockServer, + status_response: JavaStatusResponse | BedrockStatusResponse, + entity_ids: list[str], + request: pytest.FixtureRequest, + snapshot: SnapshotAssertion, +) -> None: + """Test sensor.""" + mock_config_entry = request.getfixturevalue(mock_config_entry) + mock_config_entry.add_to_hass(hass) + + with patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + for entity_id in entity_ids: + assert hass.states.get(entity_id) == snapshot + + +@pytest.mark.parametrize( + ("mock_config_entry", "server", "status_response", "entity_ids"), + [ + ( + "java_mock_config_entry", + JavaServer, + TEST_JAVA_STATUS_RESPONSE, + JAVA_SENSOR_ENTITIES_DISABLED_BY_DEFAULT, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + TEST_BEDROCK_STATUS_RESPONSE, + BEDROCK_SENSOR_ENTITIES_DISABLED_BY_DEFAULT, + ), + ], +) +async def test_sensor_disabled_by_default( + hass: HomeAssistant, + mock_config_entry: str, + server: JavaServer | BedrockServer, + status_response: JavaStatusResponse | BedrockStatusResponse, + entity_ids: list[str], + request: pytest.FixtureRequest, +) -> None: + """Test sensor, which is disabled by default.""" + mock_config_entry = request.getfixturevalue(mock_config_entry) + mock_config_entry.add_to_hass(hass) + + with patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + for entity_id in entity_ids: + assert not hass.states.get(entity_id) + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("mock_config_entry", "server", "status_response", "entity_ids"), + [ + ( + "java_mock_config_entry", + JavaServer, + TEST_JAVA_STATUS_RESPONSE, + JAVA_SENSOR_ENTITIES, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + TEST_BEDROCK_STATUS_RESPONSE, + BEDROCK_SENSOR_ENTITIES, + ), + ], +) +async def test_sensor_update( + hass: HomeAssistant, + mock_config_entry: str, + server: JavaServer | BedrockServer, + status_response: JavaStatusResponse | BedrockStatusResponse, + entity_ids: list[str], + request: pytest.FixtureRequest, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor update.""" + mock_config_entry = request.getfixturevalue(mock_config_entry) + mock_config_entry.add_to_hass(hass) + + with patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + for entity_id in entity_ids: + assert hass.states.get(entity_id) == snapshot + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("mock_config_entry", "server", "status_response", "entity_ids"), + [ + ( + "java_mock_config_entry", + JavaServer, + TEST_JAVA_STATUS_RESPONSE, + JAVA_SENSOR_ENTITIES, + ), + ( + "bedrock_mock_config_entry", + BedrockServer, + TEST_BEDROCK_STATUS_RESPONSE, + BEDROCK_SENSOR_ENTITIES, + ), + ], +) +async def test_sensor_update_failure( + hass: HomeAssistant, + mock_config_entry: str, + server: JavaServer | BedrockServer, + status_response: JavaStatusResponse | BedrockStatusResponse, + entity_ids: list[str], + request: pytest.FixtureRequest, + freezer: FrozenDateTimeFactory, +) -> None: + """Test failed sensor update.""" + mock_config_entry = request.getfixturevalue(mock_config_entry) + mock_config_entry.add_to_hass(hass) + + with patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.lookup", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + with patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + side_effect=OSError, + ): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + for entity_id in entity_ids: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE