mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add tests to bring greeneye_monitor to 99% coverage (#58661)
* Bring greeneye_monitor to 99% coverage. * Pass monitor into listeners on Monitors * Updates for changes in `dev`, create mock monitor * Remove logging left in after debugging * Remove xfails now that #58764 has merged
This commit is contained in:
parent
a29264518c
commit
a079b4fd58
@ -399,8 +399,6 @@ omit =
|
|||||||
homeassistant/components/google_travel_time/sensor.py
|
homeassistant/components/google_travel_time/sensor.py
|
||||||
homeassistant/components/gpmdp/media_player.py
|
homeassistant/components/gpmdp/media_player.py
|
||||||
homeassistant/components/gpsd/sensor.py
|
homeassistant/components/gpsd/sensor.py
|
||||||
homeassistant/components/greeneye_monitor/*
|
|
||||||
homeassistant/components/greeneye_monitor/sensor.py
|
|
||||||
homeassistant/components/greenwave/light.py
|
homeassistant/components/greenwave/light.py
|
||||||
homeassistant/components/group/notify.py
|
homeassistant/components/group/notify.py
|
||||||
homeassistant/components/growatt_server/sensor.py
|
homeassistant/components/growatt_server/sensor.py
|
||||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from greeneye import Monitors
|
import greeneye
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -123,7 +123,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: COMPONENT_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
|||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the GreenEye Monitor component."""
|
"""Set up the GreenEye Monitor component."""
|
||||||
monitors = Monitors()
|
monitors = greeneye.Monitors()
|
||||||
hass.data[DATA_GREENEYE_MONITOR] = monitors
|
hass.data[DATA_GREENEYE_MONITOR] = monitors
|
||||||
|
|
||||||
server_config = config[DOMAIN]
|
server_config = config[DOMAIN]
|
||||||
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
from typing import Any, Generic, Optional, TypeVar, cast
|
from typing import Any, Generic, Optional, TypeVar, cast
|
||||||
|
|
||||||
import greeneye
|
import greeneye
|
||||||
from greeneye import Monitors
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
from homeassistant.components.sensor import SensorEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -145,7 +144,7 @@ class GEMSensor(Generic[T], SensorEntity):
|
|||||||
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
monitors = self.hass.data[DATA_GREENEYE_MONITOR]
|
||||||
monitors.remove_listener(self._on_new_monitor)
|
monitors.remove_listener(self._on_new_monitor)
|
||||||
|
|
||||||
def _try_connect_to_monitor(self, monitors: Monitors) -> bool:
|
def _try_connect_to_monitor(self, monitors: greeneye.Monitors) -> bool:
|
||||||
monitor = monitors.monitors.get(self._monitor_serial_number)
|
monitor = monitors.monitors.get(self._monitor_serial_number)
|
||||||
if not monitor:
|
if not monitor:
|
||||||
return False
|
return False
|
||||||
|
@ -463,6 +463,9 @@ googlemaps==2.5.1
|
|||||||
# homeassistant.components.gree
|
# homeassistant.components.gree
|
||||||
greeclimate==0.12.3
|
greeclimate==0.12.3
|
||||||
|
|
||||||
|
# homeassistant.components.greeneye_monitor
|
||||||
|
greeneye_monitor==2.1
|
||||||
|
|
||||||
# homeassistant.components.growatt_server
|
# homeassistant.components.growatt_server
|
||||||
growattServer==1.1.0
|
growattServer==1.1.0
|
||||||
|
|
||||||
|
1
tests/components/greeneye_monitor/__init__.py
Normal file
1
tests/components/greeneye_monitor/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the GreenEye Monitor integration."""
|
205
tests/components/greeneye_monitor/common.py
Normal file
205
tests/components/greeneye_monitor/common.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
"""Common helpers for greeneye_monitor tests."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from homeassistant.components.greeneye_monitor import (
|
||||||
|
CONF_CHANNELS,
|
||||||
|
CONF_COUNTED_QUANTITY,
|
||||||
|
CONF_COUNTED_QUANTITY_PER_PULSE,
|
||||||
|
CONF_MONITORS,
|
||||||
|
CONF_NET_METERING,
|
||||||
|
CONF_NUMBER,
|
||||||
|
CONF_PULSE_COUNTERS,
|
||||||
|
CONF_SERIAL_NUMBER,
|
||||||
|
CONF_TEMPERATURE_SENSORS,
|
||||||
|
CONF_TIME_UNIT,
|
||||||
|
CONF_VOLTAGE_SENSORS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SENSORS,
|
||||||
|
CONF_TEMPERATURE_UNIT,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
SINGLE_MONITOR_SERIAL_NUMBER = 110011
|
||||||
|
|
||||||
|
|
||||||
|
def make_single_monitor_config_with_sensors(sensors: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Wrap the given sensor config in the boilerplate for a single monitor with serial number SINGLE_MONITOR_SERIAL_NUMBER."""
|
||||||
|
return {
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_PORT: 7513,
|
||||||
|
CONF_MONITORS: [
|
||||||
|
{
|
||||||
|
CONF_SERIAL_NUMBER: f"00{SINGLE_MONITOR_SERIAL_NUMBER}",
|
||||||
|
**sensors,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SINGLE_MONITOR_CONFIG_NO_SENSORS = make_single_monitor_config_with_sensors({})
|
||||||
|
SINGLE_MONITOR_CONFIG_PULSE_COUNTERS = make_single_monitor_config_with_sensors(
|
||||||
|
{
|
||||||
|
CONF_PULSE_COUNTERS: [
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 1,
|
||||||
|
CONF_NAME: "pulse_a",
|
||||||
|
CONF_COUNTED_QUANTITY: "pulses",
|
||||||
|
CONF_COUNTED_QUANTITY_PER_PULSE: 1.0,
|
||||||
|
CONF_TIME_UNIT: "s",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 2,
|
||||||
|
CONF_NAME: "pulse_2",
|
||||||
|
CONF_COUNTED_QUANTITY: "gal",
|
||||||
|
CONF_COUNTED_QUANTITY_PER_PULSE: 0.5,
|
||||||
|
CONF_TIME_UNIT: "min",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 3,
|
||||||
|
CONF_NAME: "pulse_3",
|
||||||
|
CONF_COUNTED_QUANTITY: "gal",
|
||||||
|
CONF_COUNTED_QUANTITY_PER_PULSE: 0.5,
|
||||||
|
CONF_TIME_UNIT: "h",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 4,
|
||||||
|
CONF_NAME: "pulse_d",
|
||||||
|
CONF_COUNTED_QUANTITY: "pulses",
|
||||||
|
CONF_COUNTED_QUANTITY_PER_PULSE: 1.0,
|
||||||
|
CONF_TIME_UNIT: "s",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SINGLE_MONITOR_CONFIG_POWER_SENSORS = make_single_monitor_config_with_sensors(
|
||||||
|
{
|
||||||
|
CONF_CHANNELS: [
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 1,
|
||||||
|
CONF_NAME: "channel 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 2,
|
||||||
|
CONF_NAME: "channel two",
|
||||||
|
CONF_NET_METERING: True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS = make_single_monitor_config_with_sensors(
|
||||||
|
{
|
||||||
|
CONF_TEMPERATURE_SENSORS: {
|
||||||
|
CONF_TEMPERATURE_UNIT: "F",
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{CONF_NUMBER: 1, CONF_NAME: "temp_a"},
|
||||||
|
{CONF_NUMBER: 2, CONF_NAME: "temp_2"},
|
||||||
|
{CONF_NUMBER: 3, CONF_NAME: "temp_c"},
|
||||||
|
{CONF_NUMBER: 4, CONF_NAME: "temp_d"},
|
||||||
|
{CONF_NUMBER: 5, CONF_NAME: "temp_5"},
|
||||||
|
{CONF_NUMBER: 6, CONF_NAME: "temp_f"},
|
||||||
|
{CONF_NUMBER: 7, CONF_NAME: "temp_g"},
|
||||||
|
{CONF_NUMBER: 8, CONF_NAME: "temp_h"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS = make_single_monitor_config_with_sensors(
|
||||||
|
{
|
||||||
|
CONF_VOLTAGE_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NUMBER: 1,
|
||||||
|
CONF_NAME: "voltage 1",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_greeneye_monitor_component_with_config(
|
||||||
|
hass: HomeAssistant, config: ConfigType
|
||||||
|
) -> bool:
|
||||||
|
"""Set up the greeneye_monitor component with the given config. Return True if successful, False otherwise."""
|
||||||
|
result = await async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def mock_with_listeners() -> MagicMock:
|
||||||
|
"""Create a MagicMock with methods that follow the same pattern for working with listeners in the greeneye_monitor API."""
|
||||||
|
mock = MagicMock()
|
||||||
|
add_listeners(mock)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
def async_mock_with_listeners() -> AsyncMock:
|
||||||
|
"""Create an AsyncMock with methods that follow the same pattern for working with listeners in the greeneye_monitor API."""
|
||||||
|
mock = AsyncMock()
|
||||||
|
add_listeners(mock)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
def add_listeners(mock: MagicMock | AsyncMock) -> None:
|
||||||
|
"""Add add_listener and remove_listener methods to the given mock that behave like their counterparts on objects from the greeneye_monitor API, plus a notify_all_listeners method that calls all registered listeners."""
|
||||||
|
mock.listeners = []
|
||||||
|
mock.add_listener = mock.listeners.append
|
||||||
|
mock.remove_listener = mock.listeners.remove
|
||||||
|
|
||||||
|
def notify_all_listeners(*args):
|
||||||
|
for listener in list(mock.listeners):
|
||||||
|
listener(*args)
|
||||||
|
|
||||||
|
mock.notify_all_listeners = notify_all_listeners
|
||||||
|
|
||||||
|
|
||||||
|
def mock_pulse_counter() -> MagicMock:
|
||||||
|
"""Create a mock GreenEye Monitor pulse counter."""
|
||||||
|
pulse_counter = mock_with_listeners()
|
||||||
|
pulse_counter.pulses = 1000
|
||||||
|
pulse_counter.pulses_per_second = 10
|
||||||
|
return pulse_counter
|
||||||
|
|
||||||
|
|
||||||
|
def mock_temperature_sensor() -> MagicMock:
|
||||||
|
"""Create a mock GreenEye Monitor temperature sensor."""
|
||||||
|
temperature_sensor = mock_with_listeners()
|
||||||
|
temperature_sensor.temperature = 32.0
|
||||||
|
return temperature_sensor
|
||||||
|
|
||||||
|
|
||||||
|
def mock_channel() -> MagicMock:
|
||||||
|
"""Create a mock GreenEye Monitor CT channel."""
|
||||||
|
channel = mock_with_listeners()
|
||||||
|
channel.absolute_watt_seconds = 1000
|
||||||
|
channel.polarized_watt_seconds = -400
|
||||||
|
channel.watts = None
|
||||||
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
def mock_monitor(serial_number: int) -> MagicMock:
|
||||||
|
"""Create a mock GreenEye Monitor."""
|
||||||
|
monitor = mock_with_listeners()
|
||||||
|
monitor.serial_number = serial_number
|
||||||
|
monitor.voltage = 120.0
|
||||||
|
monitor.pulse_counters = [mock_pulse_counter() for i in range(0, 4)]
|
||||||
|
monitor.temperature_sensors = [mock_temperature_sensor() for i in range(0, 8)]
|
||||||
|
monitor.channels = [mock_channel() for i in range(0, 32)]
|
||||||
|
return monitor
|
118
tests/components/greeneye_monitor/conftest.py
Normal file
118
tests/components/greeneye_monitor/conftest.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""Common fixtures for testing greeneye_monitor."""
|
||||||
|
from typing import Any, Dict
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.greeneye_monitor import DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
DEVICE_CLASS_POWER,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_VOLTAGE,
|
||||||
|
ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
POWER_WATT,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_registry import (
|
||||||
|
RegistryEntry,
|
||||||
|
async_get as get_entity_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .common import add_listeners
|
||||||
|
|
||||||
|
|
||||||
|
def assert_sensor_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: str,
|
||||||
|
expected_state: str,
|
||||||
|
attributes: Dict[str, Any] = {},
|
||||||
|
) -> None:
|
||||||
|
"""Assert that the given entity has the expected state and at least the provided attributes."""
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
actual_state = state.state
|
||||||
|
assert actual_state == expected_state
|
||||||
|
for (key, value) in attributes.items():
|
||||||
|
assert key in state.attributes
|
||||||
|
assert state.attributes[key] == value
|
||||||
|
|
||||||
|
|
||||||
|
def assert_temperature_sensor_registered(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
serial_number: int,
|
||||||
|
number: int,
|
||||||
|
name: str,
|
||||||
|
):
|
||||||
|
"""Assert that a temperature sensor entity was registered properly."""
|
||||||
|
sensor = assert_sensor_registered(hass, serial_number, "temp", number, name)
|
||||||
|
assert sensor.device_class == DEVICE_CLASS_TEMPERATURE
|
||||||
|
|
||||||
|
|
||||||
|
def assert_pulse_counter_registered(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
serial_number: int,
|
||||||
|
number: int,
|
||||||
|
name: str,
|
||||||
|
quantity: str,
|
||||||
|
per_time: str,
|
||||||
|
):
|
||||||
|
"""Assert that a pulse counter entity was registered properly."""
|
||||||
|
sensor = assert_sensor_registered(hass, serial_number, "pulse", number, name)
|
||||||
|
assert sensor.unit_of_measurement == f"{quantity}/{per_time}"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_power_sensor_registered(
|
||||||
|
hass: HomeAssistant, serial_number: int, number: int, name: str
|
||||||
|
) -> None:
|
||||||
|
"""Assert that a power sensor entity was registered properly."""
|
||||||
|
sensor = assert_sensor_registered(hass, serial_number, "current", number, name)
|
||||||
|
assert sensor.unit_of_measurement == POWER_WATT
|
||||||
|
assert sensor.device_class == DEVICE_CLASS_POWER
|
||||||
|
|
||||||
|
|
||||||
|
def assert_voltage_sensor_registered(
|
||||||
|
hass: HomeAssistant, serial_number: int, number: int, name: str
|
||||||
|
) -> None:
|
||||||
|
"""Assert that a voltage sensor entity was registered properly."""
|
||||||
|
sensor = assert_sensor_registered(hass, serial_number, "volts", number, name)
|
||||||
|
assert sensor.unit_of_measurement == ELECTRIC_POTENTIAL_VOLT
|
||||||
|
assert sensor.device_class == DEVICE_CLASS_VOLTAGE
|
||||||
|
|
||||||
|
|
||||||
|
def assert_sensor_registered(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
serial_number: int,
|
||||||
|
sensor_type: str,
|
||||||
|
number: int,
|
||||||
|
name: str,
|
||||||
|
) -> RegistryEntry:
|
||||||
|
"""Assert that a sensor entity of a given type was registered properly."""
|
||||||
|
registry = get_entity_registry(hass)
|
||||||
|
unique_id = f"{serial_number}-{sensor_type}-{number}"
|
||||||
|
|
||||||
|
entity_id = registry.async_get_entity_id("sensor", DOMAIN, unique_id)
|
||||||
|
assert entity_id is not None
|
||||||
|
|
||||||
|
sensor = registry.async_get(entity_id)
|
||||||
|
assert sensor
|
||||||
|
assert sensor.unique_id == unique_id
|
||||||
|
assert sensor.original_name == name
|
||||||
|
|
||||||
|
return sensor
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def monitors() -> AsyncMock:
|
||||||
|
"""Provide a mock greeneye.Monitors object that has listeners and can add new monitors."""
|
||||||
|
with patch("greeneye.Monitors", new=AsyncMock) as mock_monitors:
|
||||||
|
add_listeners(mock_monitors)
|
||||||
|
mock_monitors.monitors = {}
|
||||||
|
|
||||||
|
def add_monitor(monitor: MagicMock) -> None:
|
||||||
|
"""Add the given mock monitor as a monitor with the given serial number, notifying any listeners on the Monitors object."""
|
||||||
|
serial_number = monitor.serial_number
|
||||||
|
mock_monitors.monitors[serial_number] = monitor
|
||||||
|
mock_monitors.notify_all_listeners(monitor)
|
||||||
|
|
||||||
|
mock_monitors.add_monitor = add_monitor
|
||||||
|
yield mock_monitors
|
199
tests/components/greeneye_monitor/test_init.py
Normal file
199
tests/components/greeneye_monitor/test_init.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
"""Tests for greeneye_monitor component initialization."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.greeneye_monitor import (
|
||||||
|
CONF_MONITORS,
|
||||||
|
CONF_NUMBER,
|
||||||
|
CONF_SERIAL_NUMBER,
|
||||||
|
CONF_TEMPERATURE_SENSORS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SENSORS,
|
||||||
|
CONF_TEMPERATURE_UNIT,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
SINGLE_MONITOR_CONFIG_NO_SENSORS,
|
||||||
|
SINGLE_MONITOR_CONFIG_POWER_SENSORS,
|
||||||
|
SINGLE_MONITOR_CONFIG_PULSE_COUNTERS,
|
||||||
|
SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS,
|
||||||
|
SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS,
|
||||||
|
SINGLE_MONITOR_SERIAL_NUMBER,
|
||||||
|
setup_greeneye_monitor_component_with_config,
|
||||||
|
)
|
||||||
|
from .conftest import (
|
||||||
|
assert_power_sensor_registered,
|
||||||
|
assert_pulse_counter_registered,
|
||||||
|
assert_temperature_sensor_registered,
|
||||||
|
assert_voltage_sensor_registered,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_fails_if_no_sensors_defined(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that component setup fails if there are no sensors defined in the YAML."""
|
||||||
|
success = await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_NO_SENSORS
|
||||||
|
)
|
||||||
|
assert not success
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(reason="Currently failing. Will fix in subsequent PR.")
|
||||||
|
async def test_setup_succeeds_no_config(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that component setup succeeds if there is no config present in the YAML."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_creates_temperature_entities(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that component setup registers temperature sensors properly."""
|
||||||
|
assert await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "temp_a"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "temp_2"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 3, "temp_c"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 4, "temp_d"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 5, "temp_5"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 6, "temp_f"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 7, "temp_g"
|
||||||
|
)
|
||||||
|
assert_temperature_sensor_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 8, "temp_h"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_creates_pulse_counter_entities(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that component setup registers pulse counters properly."""
|
||||||
|
assert await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_pulse_counter_registered(
|
||||||
|
hass,
|
||||||
|
SINGLE_MONITOR_SERIAL_NUMBER,
|
||||||
|
1,
|
||||||
|
"pulse_a",
|
||||||
|
"pulses",
|
||||||
|
"s",
|
||||||
|
)
|
||||||
|
assert_pulse_counter_registered(
|
||||||
|
hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "pulse_2", "gal", "min"
|
||||||
|
)
|
||||||
|
assert_pulse_counter_registered(
|
||||||
|
hass,
|
||||||
|
SINGLE_MONITOR_SERIAL_NUMBER,
|
||||||
|
3,
|
||||||
|
"pulse_3",
|
||||||
|
"gal",
|
||||||
|
"h",
|
||||||
|
)
|
||||||
|
assert_pulse_counter_registered(
|
||||||
|
hass,
|
||||||
|
SINGLE_MONITOR_SERIAL_NUMBER,
|
||||||
|
4,
|
||||||
|
"pulse_d",
|
||||||
|
"pulses",
|
||||||
|
"s",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_creates_power_sensor_entities(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that component setup registers power sensors correctly."""
|
||||||
|
assert await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_power_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "channel 1")
|
||||||
|
assert_power_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 2, "channel two")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_creates_voltage_sensor_entities(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that component setup registers voltage sensors properly."""
|
||||||
|
assert await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_voltage_sensor_registered(hass, SINGLE_MONITOR_SERIAL_NUMBER, 1, "voltage 1")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_multi_monitor_config(hass: HomeAssistant, monitors: AsyncMock) -> None:
|
||||||
|
"""Test that component setup registers entities from multiple monitors correctly."""
|
||||||
|
assert await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_PORT: 7513,
|
||||||
|
CONF_MONITORS: [
|
||||||
|
{
|
||||||
|
CONF_SERIAL_NUMBER: "00000001",
|
||||||
|
CONF_TEMPERATURE_SENSORS: {
|
||||||
|
CONF_TEMPERATURE_UNIT: "C",
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{CONF_NUMBER: 1, CONF_NAME: "unit_1_temp_1"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_SERIAL_NUMBER: "00000002",
|
||||||
|
CONF_TEMPERATURE_SENSORS: {
|
||||||
|
CONF_TEMPERATURE_UNIT: "F",
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{CONF_NUMBER: 1, CONF_NAME: "unit_2_temp_1"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_temperature_sensor_registered(hass, 1, 1, "unit_1_temp_1")
|
||||||
|
assert_temperature_sensor_registered(hass, 2, 1, "unit_2_temp_1")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_and_shutdown(hass: HomeAssistant, monitors: AsyncMock) -> None:
|
||||||
|
"""Test that the component can set up and shut down cleanly, closing the underlying server on shutdown."""
|
||||||
|
server = AsyncMock()
|
||||||
|
monitors.start_server = AsyncMock(return_value=server)
|
||||||
|
assert await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_stop()
|
||||||
|
|
||||||
|
assert server.close.called
|
165
tests/components/greeneye_monitor/test_sensor.py
Normal file
165
tests/components/greeneye_monitor/test_sensor.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
"""Tests for greeneye_monitor sensors."""
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from homeassistant.components.greeneye_monitor.sensor import (
|
||||||
|
DATA_PULSES,
|
||||||
|
DATA_WATT_SECONDS,
|
||||||
|
)
|
||||||
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_registry import async_get as get_entity_registry
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
SINGLE_MONITOR_CONFIG_POWER_SENSORS,
|
||||||
|
SINGLE_MONITOR_CONFIG_PULSE_COUNTERS,
|
||||||
|
SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS,
|
||||||
|
SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS,
|
||||||
|
SINGLE_MONITOR_SERIAL_NUMBER,
|
||||||
|
mock_monitor,
|
||||||
|
setup_greeneye_monitor_component_with_config,
|
||||||
|
)
|
||||||
|
from .conftest import assert_sensor_state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disable_sensor_before_monitor_connected(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that a sensor disabled before its monitor connected stops listening for new monitors."""
|
||||||
|
# The sensor base class handles connecting the monitor, so we test this with a single voltage sensor for ease
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(monitors.listeners) == 1
|
||||||
|
await disable_entity(hass, "sensor.voltage_1")
|
||||||
|
assert len(monitors.listeners) == 0 # Make sure we cleaned up the listener
|
||||||
|
|
||||||
|
|
||||||
|
async def test_updates_state_when_monitor_connected(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that a sensor updates its state when its monitor first connects."""
|
||||||
|
# The sensor base class handles updating the state on connection, so we test this with a single voltage sensor for ease
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_sensor_state(hass, "sensor.voltage_1", STATE_UNKNOWN)
|
||||||
|
assert len(monitors.listeners) == 1
|
||||||
|
connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
assert len(monitors.listeners) == 0 # Make sure we cleaned up the listener
|
||||||
|
assert_sensor_state(hass, "sensor.voltage_1", "120.0")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_disable_sensor_after_monitor_connected(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that a sensor disabled after its monitor connected stops listening for sensor changes."""
|
||||||
|
# The sensor base class handles connecting the monitor, so we test this with a single voltage sensor for ease
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS
|
||||||
|
)
|
||||||
|
monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
|
||||||
|
assert len(monitor.listeners) == 1
|
||||||
|
await disable_entity(hass, "sensor.voltage_1")
|
||||||
|
assert len(monitor.listeners) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_updates_state_when_sensor_pushes(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that a sensor entity updates its state when the underlying sensor pushes an update."""
|
||||||
|
# The sensor base class handles triggering state updates, so we test this with a single voltage sensor for ease
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS
|
||||||
|
)
|
||||||
|
monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
assert_sensor_state(hass, "sensor.voltage_1", "120.0")
|
||||||
|
|
||||||
|
monitor.voltage = 119.8
|
||||||
|
monitor.notify_all_listeners()
|
||||||
|
assert_sensor_state(hass, "sensor.voltage_1", "119.8")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_power_sensor_initially_unknown(
|
||||||
|
hass: HomeAssistant, monitors: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that the power sensor can handle its initial state being unknown (since the GEM API needs at least two packets to arrive before it can compute watts)."""
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS
|
||||||
|
)
|
||||||
|
connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
assert_sensor_state(
|
||||||
|
hass, "sensor.channel_1", STATE_UNKNOWN, {DATA_WATT_SECONDS: 1000}
|
||||||
|
)
|
||||||
|
# This sensor was configured with net metering on, so we should be taking the
|
||||||
|
# polarized value
|
||||||
|
assert_sensor_state(
|
||||||
|
hass, "sensor.channel_two", STATE_UNKNOWN, {DATA_WATT_SECONDS: -400}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_power_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None:
|
||||||
|
"""Test that a power sensor reports its values correctly, including handling net metering."""
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_POWER_SENSORS
|
||||||
|
)
|
||||||
|
monitor = connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
monitor.channels[0].watts = 120.0
|
||||||
|
monitor.channels[1].watts = 120.0
|
||||||
|
monitor.channels[0].notify_all_listeners()
|
||||||
|
monitor.channels[1].notify_all_listeners()
|
||||||
|
assert_sensor_state(hass, "sensor.channel_1", "120.0", {DATA_WATT_SECONDS: 1000})
|
||||||
|
# This sensor was configured with net metering on, so we should be taking the
|
||||||
|
# polarized value
|
||||||
|
assert_sensor_state(hass, "sensor.channel_two", "120.0", {DATA_WATT_SECONDS: -400})
|
||||||
|
|
||||||
|
|
||||||
|
async def test_pulse_counter(hass: HomeAssistant, monitors: AsyncMock) -> None:
|
||||||
|
"""Test that a pulse counter sensor reports its values properly, including calculating different units."""
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_PULSE_COUNTERS
|
||||||
|
)
|
||||||
|
connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
assert_sensor_state(hass, "sensor.pulse_a", "10.0", {DATA_PULSES: 1000})
|
||||||
|
# This counter was configured with each pulse meaning 0.5 gallons and
|
||||||
|
# wanting to show gallons per minute, so 10 pulses per second -> 300 gal/min
|
||||||
|
assert_sensor_state(hass, "sensor.pulse_2", "300.0", {DATA_PULSES: 1000})
|
||||||
|
# This counter was configured with each pulse meaning 0.5 gallons and
|
||||||
|
# wanting to show gallons per hour, so 10 pulses per second -> 18000 gal/hr
|
||||||
|
assert_sensor_state(hass, "sensor.pulse_3", "18000.0", {DATA_PULSES: 1000})
|
||||||
|
|
||||||
|
|
||||||
|
async def test_temperature_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None:
|
||||||
|
"""Test that a temperature sensor reports its values properly, including proper handling of when its native unit is different from that configured in hass."""
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_TEMPERATURE_SENSORS
|
||||||
|
)
|
||||||
|
connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
# The config says that the sensor is reporting in Fahrenheit; if we set that up
|
||||||
|
# properly, HA will have converted that to Celsius by default.
|
||||||
|
assert_sensor_state(hass, "sensor.temp_a", "0.0")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_voltage_sensor(hass: HomeAssistant, monitors: AsyncMock) -> None:
|
||||||
|
"""Test that a voltage sensor reports its values properly."""
|
||||||
|
await setup_greeneye_monitor_component_with_config(
|
||||||
|
hass, SINGLE_MONITOR_CONFIG_VOLTAGE_SENSORS
|
||||||
|
)
|
||||||
|
connect_monitor(monitors, SINGLE_MONITOR_SERIAL_NUMBER)
|
||||||
|
assert_sensor_state(hass, "sensor.voltage_1", "120.0")
|
||||||
|
|
||||||
|
|
||||||
|
def connect_monitor(monitors: AsyncMock, serial_number: int) -> MagicMock:
|
||||||
|
"""Simulate a monitor connecting to Home Assistant. Returns the mock monitor API object."""
|
||||||
|
monitor = mock_monitor(serial_number)
|
||||||
|
monitors.add_monitor(monitor)
|
||||||
|
return monitor
|
||||||
|
|
||||||
|
|
||||||
|
async def disable_entity(hass: HomeAssistant, entity_id: str) -> None:
|
||||||
|
"""Disable the given entity."""
|
||||||
|
entity_registry = get_entity_registry(hass)
|
||||||
|
entity_registry.async_update_entity(entity_id, disabled_by="user")
|
||||||
|
await hass.async_block_till_done()
|
Loading…
x
Reference in New Issue
Block a user