diff --git a/.coveragerc b/.coveragerc index 8088bbece78..bf51d5ad594 100644 --- a/.coveragerc +++ b/.coveragerc @@ -666,7 +666,6 @@ omit = homeassistant/components/mysensors/helpers.py homeassistant/components/mysensors/light.py homeassistant/components/mysensors/notify.py - homeassistant/components/mysensors/sensor.py homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 49c32301442..1843e495801 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -3,13 +3,14 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Generator import json -from typing import Any +from typing import Any, Callable from unittest.mock import MagicMock, patch from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest +from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors import CONF_VERSION, DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( @@ -27,14 +28,14 @@ from tests.common import MockConfigEntry, load_fixture @pytest.fixture(autouse=True) -def device_tracker_storage(mock_device_tracker_conf): +def device_tracker_storage(mock_device_tracker_conf: list[Device]) -> list[Device]: """Mock out device tracker known devices storage.""" devices = mock_device_tracker_conf return devices @pytest.fixture(name="mqtt") -def mock_mqtt_fixture(hass) -> None: +def mock_mqtt_fixture(hass: HomeAssistant) -> None: """Mock the MQTT integration.""" hass.config.components.add(MQTT_DOMAIN) @@ -75,14 +76,14 @@ def mock_gateway_features( ) -> None: """Mock the gateway features.""" - async def mock_start_persistence(): + async def mock_start_persistence() -> None: """Load nodes from via persistence.""" gateway = transport_class.call_args[0][0] gateway.sensors.update(nodes) tasks.start_persistence.side_effect = mock_start_persistence - async def mock_start(): + async def mock_start() -> None: """Mock the start method.""" gateway = transport_class.call_args[0][0] gateway.on_conn_made(gateway) @@ -97,7 +98,7 @@ def transport_fixture(serial_transport: MagicMock) -> MagicMock: @pytest.fixture(name="serial_entry") -async def serial_entry_fixture(hass) -> MockConfigEntry: +async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" entry = MockConfigEntry( domain=DOMAIN, @@ -120,15 +121,25 @@ def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: @pytest.fixture async def integration( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry -) -> AsyncGenerator[MockConfigEntry, None]: +) -> AsyncGenerator[tuple[MockConfigEntry, Callable[[str], None]], None]: """Set up the mysensors integration with a config entry.""" device = config_entry.data[CONF_DEVICE] config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} config_entry.add_to_hass(hass) + + def receive_message(message_string: str) -> None: + """Receive a message with the transport. + + The message_string parameter is a string in the MySensors message format. + """ + gateway = transport.call_args[0][0] + # node_id;child_id;command;ack;type;payload\n + gateway.logic(message_string) + with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - yield config_entry + yield config_entry, receive_message def load_nodes_state(fixture_path: str) -> dict: @@ -151,7 +162,7 @@ def gps_sensor_state_fixture() -> dict: @pytest.fixture -def gps_sensor(gateway_nodes, gps_sensor_state) -> Sensor: +def gps_sensor(gateway_nodes: dict[int, Sensor], gps_sensor_state: dict) -> Sensor: """Load the gps sensor.""" nodes = update_gateway_nodes(gateway_nodes, gps_sensor_state) node = nodes[1] @@ -165,8 +176,70 @@ def power_sensor_state_fixture() -> dict: @pytest.fixture -def power_sensor(gateway_nodes, power_sensor_state) -> Sensor: +def power_sensor(gateway_nodes: dict[int, Sensor], power_sensor_state: dict) -> Sensor: """Load the power sensor.""" nodes = update_gateway_nodes(gateway_nodes, power_sensor_state) node = nodes[1] return node + + +@pytest.fixture(name="energy_sensor_state", scope="session") +def energy_sensor_state_fixture() -> dict: + """Load the energy sensor state.""" + return load_nodes_state("mysensors/energy_sensor_state.json") + + +@pytest.fixture +def energy_sensor( + gateway_nodes: dict[int, Sensor], energy_sensor_state: dict +) -> Sensor: + """Load the energy sensor.""" + nodes = update_gateway_nodes(gateway_nodes, energy_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="sound_sensor_state", scope="session") +def sound_sensor_state_fixture() -> dict: + """Load the sound sensor state.""" + return load_nodes_state("mysensors/sound_sensor_state.json") + + +@pytest.fixture +def sound_sensor(gateway_nodes: dict[int, Sensor], sound_sensor_state: dict) -> Sensor: + """Load the sound sensor.""" + nodes = update_gateway_nodes(gateway_nodes, sound_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="distance_sensor_state", scope="session") +def distance_sensor_state_fixture() -> dict: + """Load the distance sensor state.""" + return load_nodes_state("mysensors/distance_sensor_state.json") + + +@pytest.fixture +def distance_sensor( + gateway_nodes: dict[int, Sensor], distance_sensor_state: dict +) -> Sensor: + """Load the distance sensor.""" + nodes = update_gateway_nodes(gateway_nodes, distance_sensor_state) + node = nodes[1] + return node + + +@pytest.fixture(name="temperature_sensor_state", scope="session") +def temperature_sensor_state_fixture() -> dict: + """Load the temperature sensor state.""" + return load_nodes_state("mysensors/temperature_sensor_state.json") + + +@pytest.fixture +def temperature_sensor( + gateway_nodes: dict[int, Sensor], temperature_sensor_state: dict +) -> Sensor: + """Load the temperature sensor.""" + nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 6edddc68592..880226ced60 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -1,31 +1,161 @@ """Provide tests for mysensors sensor platform.""" +from __future__ import annotations +from typing import Callable -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem + +from tests.common import MockConfigEntry -async def test_gps_sensor(hass, gps_sensor, integration): +async def test_gps_sensor( + hass: HomeAssistant, + gps_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: """Test a gps sensor.""" entity_id = "sensor.gps_sensor_1_1" + _, receive_message = integration state = hass.states.get(entity_id) + assert state assert state.state == "40.741894,-73.989311,12" + altitude = 0 + new_coords = "40.782,-73.965" + message_string = f"1;1;1;0;49;{new_coords},{altitude}\n" -async def test_power_sensor(hass, power_sensor, integration): + receive_message(message_string) + # the integration adds multiple jobs to do the update currently + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == f"{new_coords},{altitude}" + + +async def test_power_sensor( + hass: HomeAssistant, + power_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: """Test a power sensor.""" entity_id = "sensor.power_sensor_1_1" state = hass.states.get(entity_id) + assert state assert state.state == "1200" assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert ATTR_LAST_RESET not in state.attributes + + +async def test_energy_sensor( + hass: HomeAssistant, + energy_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test an energy sensor.""" + entity_id = "sensor.energy_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "18000" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_LAST_RESET] == utc_from_timestamp(0).isoformat() + + +async def test_sound_sensor( + hass: HomeAssistant, + sound_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test a sound sensor.""" + entity_id = "sensor.sound_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "10" + assert state.attributes[ATTR_ICON] == "mdi:volume-high" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "dB" + + +async def test_distance_sensor( + hass: HomeAssistant, + distance_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], +) -> None: + """Test a distance sensor.""" + entity_id = "sensor.distance_sensor_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "15" + assert state.attributes[ATTR_ICON] == "mdi:ruler" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "cm" + + +@pytest.mark.parametrize( + "unit_system, unit", + [(METRIC_SYSTEM, TEMP_CELSIUS), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT)], +) +async def test_temperature_sensor( + hass: HomeAssistant, + temperature_sensor: Sensor, + integration: tuple[MockConfigEntry, Callable[[str], None]], + unit_system: UnitSystem, + unit: str, +) -> None: + """Test a temperature sensor.""" + entity_id = "sensor.temperature_sensor_1_1" + hass.config.units = unit_system + _, receive_message = integration + temperature = "22.0" + message_string = f"1;1;1;0;0;{temperature}\n" + + receive_message(message_string) + # the integration adds multiple jobs to do the update currently + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == temperature + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT diff --git a/tests/fixtures/mysensors/distance_sensor_state.json b/tests/fixtures/mysensors/distance_sensor_state.json new file mode 100644 index 00000000000..ff8b246b880 --- /dev/null +++ b/tests/fixtures/mysensors/distance_sensor_state.json @@ -0,0 +1,22 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 15, + "description": "", + "values": { + "13": "15", + "43": "cm" + } + } + }, + "type": 17, + "sketch_name": "Distance Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/energy_sensor_state.json b/tests/fixtures/mysensors/energy_sensor_state.json new file mode 100644 index 00000000000..063083c9c1e --- /dev/null +++ b/tests/fixtures/mysensors/energy_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 13, + "description": "", + "values": { + "18": "18000" + } + } + }, + "type": 17, + "sketch_name": "Energy Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/sound_sensor_state.json b/tests/fixtures/mysensors/sound_sensor_state.json new file mode 100644 index 00000000000..35651243250 --- /dev/null +++ b/tests/fixtures/mysensors/sound_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 33, + "description": "", + "values": { + "37": "10" + } + } + }, + "type": 17, + "sketch_name": "Sound Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/fixtures/mysensors/temperature_sensor_state.json b/tests/fixtures/mysensors/temperature_sensor_state.json new file mode 100644 index 00000000000..4367be6a3cd --- /dev/null +++ b/tests/fixtures/mysensors/temperature_sensor_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 6, + "description": "", + "values": { + "0": "20.0" + } + } + }, + "type": 17, + "sketch_name": "Temperature Sensor", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +}