From f1a4ba8bb0a7b241e69eacc75f194a78d0490535 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Aug 2021 13:19:31 -0700 Subject: [PATCH] Add Rainforest Eagle tests and price (#54887) --- .coveragerc | 3 - .../components/rainforest_eagle/data.py | 4 +- .../components/rainforest_eagle/sensor.py | 21 ++- homeassistant/helpers/update_coordinator.py | 7 +- tests/components/rainforest_eagle/__init__.py | 63 +++++++ .../rainforest_eagle/test_config_flow.py | 45 +---- .../rainforest_eagle/test_sensor.py | 158 ++++++++++++++++++ 7 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 tests/components/rainforest_eagle/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 9410428a299..237e676c9ac 100644 --- a/.coveragerc +++ b/.coveragerc @@ -838,9 +838,6 @@ omit = homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py - homeassistant/components/rainforest_eagle/__init__.py - homeassistant/components/rainforest_eagle/data.py - homeassistant/components/rainforest_eagle/sensor.py homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index b252993d888..e4cfe144a5e 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -147,14 +147,14 @@ class EagleDataCoordinator(DataUpdateCoordinator): async def _async_update_data_100(self): """Get the latest data from the Eagle-100 device.""" try: - data = await self.hass.async_add_executor_job(self._fetch_data) + data = await self.hass.async_add_executor_job(self._fetch_data_100) except UPDATE_100_ERRORS as error: raise UpdateFailed from error _LOGGER.debug("API data: %s", data) return data - def _fetch_data(self): + def _fetch_data_100(self): """Fetch and return the four sensor values in a dict.""" if self.eagle100_reader is None: self.eagle100_reader = Eagle100Reader( diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 6946ee03974..67f61ffdc29 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, @@ -39,6 +40,7 @@ SENSORS = ( name="Meter Power Demand", native_unit_of_measurement=POWER_KILO_WATT, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="zigbee:CurrentSummationDelivered", @@ -95,7 +97,22 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities(EagleSensor(coordinator, description) for description in SENSORS) + entities = [EagleSensor(coordinator, description) for description in SENSORS] + + if coordinator.data.get("zigbee:Price") not in (None, "invalid"): + entities.append( + EagleSensor( + coordinator, + SensorEntityDescription( + key="zigbee:Price", + name="Meter Price", + native_unit_of_measurement=f"{coordinator.data['zigbee:PriceCurrency']}/{ENERGY_KILO_WATT_HOUR}", + state_class=STATE_CLASS_MEASUREMENT, + ), + ) + ) + + async_add_entities(entities) class EagleSensor(CoordinatorEntity, SensorEntity): @@ -111,7 +128,7 @@ class EagleSensor(CoordinatorEntity, SensorEntity): @property def unique_id(self) -> str | None: """Return unique ID of entity.""" - return f"{self.coordinator.cloud_id}-{self.entity_description.key}" + return f"{self.coordinator.cloud_id}-${self.coordinator.hardware_address}-{self.entity_description.key}" @property def native_value(self) -> StateType: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 69111b00885..2203ab240ef 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -242,10 +242,9 @@ class DataUpdateCoordinator(Generic[T]): except Exception as err: # pylint: disable=broad-except self.last_exception = err self.last_update_success = False - if log_failures: - self.logger.exception( - "Unexpected error fetching %s data: %s", self.name, err - ) + self.logger.exception( + "Unexpected error fetching %s data: %s", self.name, err + ) else: if not self.last_update_success: diff --git a/tests/components/rainforest_eagle/__init__.py b/tests/components/rainforest_eagle/__init__.py index df4f1749d49..c5e41591789 100644 --- a/tests/components/rainforest_eagle/__init__.py +++ b/tests/components/rainforest_eagle/__init__.py @@ -1 +1,64 @@ """Tests for the Rainforest Eagle integration.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.rainforest_eagle.const import ( + CONF_CLOUD_ID, + CONF_HARDWARE_ADDRESS, + CONF_INSTALL_CODE, + DOMAIN, + TYPE_EAGLE_200, +) +from homeassistant.const import CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.setup import async_setup_component + + +async def test_import(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.rainforest_eagle.data.async_get_type", + return_value=(TYPE_EAGLE_200, "mock-hw"), + ), patch( + "homeassistant.components.rainforest_eagle.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "ip_address": "192.168.1.55", + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + } + }, + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + + assert entry.title == "abcdef" + assert entry.data == { + CONF_TYPE: TYPE_EAGLE_200, + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + CONF_HARDWARE_ADDRESS: "mock-hw", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Second time we should get already_configured + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + data={CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, + context={"source": config_entries.SOURCE_IMPORT}, + ) + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 626069ed6c1..0a294875f76 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.rainforest_eagle.const import ( from homeassistant.components.rainforest_eagle.data import CannotConnect, InvalidAuth from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM async def test_form(hass: HomeAssistant) -> None: @@ -88,42 +84,3 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_import(hass: HomeAssistant) -> None: - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.rainforest_eagle.data.async_get_type", - return_value=(TYPE_EAGLE_200, "mock-hw"), - ), patch( - "homeassistant.components.rainforest_eagle.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, - context={"source": config_entries.SOURCE_IMPORT}, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "abcdef" - assert result["data"] == { - CONF_TYPE: TYPE_EAGLE_200, - CONF_CLOUD_ID: "abcdef", - CONF_INSTALL_CODE: "123456", - CONF_HARDWARE_ADDRESS: "mock-hw", - } - assert len(mock_setup_entry.mock_calls) == 1 - - # Second time we should get already_configured - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - data={CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, - context={"source": config_entries.SOURCE_IMPORT}, - ) - - assert result2["type"] == RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" diff --git a/tests/components/rainforest_eagle/test_sensor.py b/tests/components/rainforest_eagle/test_sensor.py new file mode 100644 index 00000000000..46621eb5fdc --- /dev/null +++ b/tests/components/rainforest_eagle/test_sensor.py @@ -0,0 +1,158 @@ +"""Tests for rainforest eagle sensors.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.rainforest_eagle.const import ( + CONF_CLOUD_ID, + CONF_HARDWARE_ADDRESS, + CONF_INSTALL_CODE, + DOMAIN, + TYPE_EAGLE_100, + TYPE_EAGLE_200, +) +from homeassistant.const import CONF_TYPE +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +MOCK_CLOUD_ID = "12345" +MOCK_200_RESPONSE_WITH_PRICE = { + "zigbee:InstantaneousDemand": { + "Name": "zigbee:InstantaneousDemand", + "Value": "1.152000", + }, + "zigbee:CurrentSummationDelivered": { + "Name": "zigbee:CurrentSummationDelivered", + "Value": "45251.285000", + }, + "zigbee:CurrentSummationReceived": { + "Name": "zigbee:CurrentSummationReceived", + "Value": "232.232000", + }, + "zigbee:Price": {"Name": "zigbee:Price", "Value": "0.053990"}, + "zigbee:PriceCurrency": {"Name": "zigbee:PriceCurrency", "Value": "USD"}, +} +MOCK_200_RESPONSE_WITHOUT_PRICE = { + "zigbee:InstantaneousDemand": { + "Name": "zigbee:InstantaneousDemand", + "Value": "1.152000", + }, + "zigbee:CurrentSummationDelivered": { + "Name": "zigbee:CurrentSummationDelivered", + "Value": "45251.285000", + }, + "zigbee:CurrentSummationReceived": { + "Name": "zigbee:CurrentSummationReceived", + "Value": "232.232000", + }, + "zigbee:Price": {"Name": "zigbee:Price", "Value": "invalid"}, + "zigbee:PriceCurrency": {"Name": "zigbee:PriceCurrency", "Value": "USD"}, +} + + +@pytest.fixture +async def setup_rainforest_200(hass): + """Set up rainforest.""" + MockConfigEntry( + domain="rainforest_eagle", + data={ + CONF_CLOUD_ID: MOCK_CLOUD_ID, + CONF_INSTALL_CODE: "abcdefgh", + CONF_HARDWARE_ADDRESS: "mock-hw-address", + CONF_TYPE: TYPE_EAGLE_200, + }, + ).add_to_hass(hass) + with patch( + "aioeagle.ElectricMeter.get_device_query", + return_value=MOCK_200_RESPONSE_WITHOUT_PRICE, + ) as mock_update: + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + yield mock_update + + +@pytest.fixture +async def setup_rainforest_100(hass): + """Set up rainforest.""" + MockConfigEntry( + domain="rainforest_eagle", + data={ + CONF_CLOUD_ID: MOCK_CLOUD_ID, + CONF_INSTALL_CODE: "abcdefgh", + CONF_HARDWARE_ADDRESS: None, + CONF_TYPE: TYPE_EAGLE_100, + }, + ).add_to_hass(hass) + with patch( + "homeassistant.components.rainforest_eagle.data.Eagle100Reader", + return_value=Mock( + get_instantaneous_demand=Mock( + return_value={"InstantaneousDemand": {"Demand": "1.152000"}} + ), + get_current_summation=Mock( + return_value={ + "CurrentSummation": { + "SummationDelivered": "45251.285000", + "SummationReceived": "232.232000", + } + } + ), + ), + ) as mock_update: + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + yield mock_update + + +async def test_sensors_200(hass, setup_rainforest_200): + """Test the sensors.""" + assert len(hass.states.async_all()) == 3 + + demand = hass.states.get("sensor.meter_power_demand") + assert demand is not None + assert demand.state == "1.152000" + assert demand.attributes["unit_of_measurement"] == "kW" + + delivered = hass.states.get("sensor.total_meter_energy_delivered") + assert delivered is not None + assert delivered.state == "45251.285000" + assert delivered.attributes["unit_of_measurement"] == "kWh" + + received = hass.states.get("sensor.total_meter_energy_received") + assert received is not None + assert received.state == "232.232000" + assert received.attributes["unit_of_measurement"] == "kWh" + + setup_rainforest_200.return_value = MOCK_200_RESPONSE_WITH_PRICE + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 4 + + price = hass.states.get("sensor.meter_price") + assert price is not None + assert price.state == "0.053990" + assert price.attributes["unit_of_measurement"] == "USD/kWh" + + +async def test_sensors_100(hass, setup_rainforest_100): + """Test the sensors.""" + assert len(hass.states.async_all()) == 3 + + demand = hass.states.get("sensor.meter_power_demand") + assert demand is not None + assert demand.state == "1.152000" + assert demand.attributes["unit_of_measurement"] == "kW" + + delivered = hass.states.get("sensor.total_meter_energy_delivered") + assert delivered is not None + assert delivered.state == "45251.285000" + assert delivered.attributes["unit_of_measurement"] == "kWh" + + received = hass.states.get("sensor.total_meter_energy_received") + assert received is not None + assert received.state == "232.232000" + assert received.attributes["unit_of_measurement"] == "kWh"