diff --git a/CODEOWNERS b/CODEOWNERS index e1e1d7282d3..2d9bcf41db8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -798,8 +798,8 @@ build.json @home-assistant/supervisor /tests/components/omnilogic/ @oliver84 @djtimca @gentoosu /homeassistant/components/onboarding/ @home-assistant/core /tests/components/onboarding/ @home-assistant/core -/homeassistant/components/oncue/ @bdraco -/tests/components/oncue/ @bdraco +/homeassistant/components/oncue/ @bdraco @peterager +/tests/components/oncue/ @bdraco @peterager /homeassistant/components/ondilo_ico/ @JeromeHXP /tests/components/ondilo_ico/ @JeromeHXP /homeassistant/components/onewire/ @garbled1 @epenet diff --git a/homeassistant/components/oncue/const.py b/homeassistant/components/oncue/const.py index 5adabc84bcf..bf248369987 100644 --- a/homeassistant/components/oncue/const.py +++ b/homeassistant/components/oncue/const.py @@ -7,3 +7,7 @@ import aiohttp DOMAIN = "oncue" CONNECTION_EXCEPTIONS = (asyncio.TimeoutError, aiohttp.ClientError) + +CONNECTION_ESTABLISHED_KEY: str = "NetworkConnectionEstablished" + +VALUE_UNAVAILABLE: str = "--" diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index d1942c532e7..60a3826df42 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DOMAIN +from .const import CONNECTION_ESTABLISHED_KEY, DOMAIN, VALUE_UNAVAILABLE class OncueEntity(CoordinatorEntity, Entity): @@ -53,3 +53,23 @@ class OncueEntity(CoordinatorEntity, Entity): device: OncueDevice = self.coordinator.data[self._device_id] sensor: OncueSensor = device.sensors[self.entity_description.key] return sensor.value + + @property + def available(self) -> bool: + """Return if entity is available.""" + # The binary sensor that tracks the connection should not go unavailable. + if self.entity_description.key != CONNECTION_ESTABLISHED_KEY: + # If Kohler returns -- the entity is unavailable. + if self._oncue_value == VALUE_UNAVAILABLE: + return False + # If the cloud is reporting that the generator is not connected + # this also indicates the data is not available. + # The battery voltage sensor reports 0.0 rather than -- hence the purpose of this check. + device: OncueDevice = self.coordinator.data[self._device_id] + conn_established: OncueSensor = device.sensors[CONNECTION_ESTABLISHED_KEY] + if ( + conn_established is not None + and conn_established.value == VALUE_UNAVAILABLE + ): + return False + return super().available diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index e0533129d94..26a55cd0a96 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -10,7 +10,7 @@ ], "documentation": "https://www.home-assistant.io/integrations/oncue", "requirements": ["aiooncue==0.3.4"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco","@peterager"], "iot_class": "cloud_polling", "loggers": ["aiooncue"] } diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index 32845aa8d26..2ddaf1987f8 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -533,6 +533,270 @@ MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE = { ) } +MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE = { + "456789": OncueDevice( + name="My Generator", + state="Off", + product_name="RDC 2.4", + hardware_version="319", + serial_number="SERIAL", + sensors={ + "Product": OncueSensor( + name="Product", + display_name="Controller Type", + value="--", + display_value="RDC 2.4", + unit=None, + ), + "FirmwareVersion": OncueSensor( + name="FirmwareVersion", + display_name="Current Firmware", + value="--", + display_value="2.0.6", + unit=None, + ), + "LatestFirmware": OncueSensor( + name="LatestFirmware", + display_name="Latest Firmware", + value="--", + display_value="2.0.6", + unit=None, + ), + "EngineSpeed": OncueSensor( + name="EngineSpeed", + display_name="Engine Speed", + value="--", + display_value="0 R/min", + unit="R/min", + ), + "EngineTargetSpeed": OncueSensor( + name="EngineTargetSpeed", + display_name="Engine Target Speed", + value="--", + display_value="0 R/min", + unit="R/min", + ), + "EngineOilPressure": OncueSensor( + name="EngineOilPressure", + display_name="Engine Oil Pressure", + value="--", + display_value="0 Psi", + unit="Psi", + ), + "EngineCoolantTemperature": OncueSensor( + name="EngineCoolantTemperature", + display_name="Engine Coolant Temperature", + value="--", + display_value="32 F", + unit="F", + ), + "BatteryVoltage": OncueSensor( + name="BatteryVoltage", + display_name="Battery Voltage", + value="0.0", + display_value="13.4 V", + unit="V", + ), + "LubeOilTemperature": OncueSensor( + name="LubeOilTemperature", + display_name="Lube Oil Temperature", + value="--", + display_value="32 F", + unit="F", + ), + "GensetControllerTemperature": OncueSensor( + name="GensetControllerTemperature", + display_name="Generator Controller Temperature", + value="--", + display_value="84.2 F", + unit="F", + ), + "EngineCompartmentTemperature": OncueSensor( + name="EngineCompartmentTemperature", + display_name="Engine Compartment Temperature", + value="--", + display_value="62.6 F", + unit="F", + ), + "GeneratorTrueTotalPower": OncueSensor( + name="GeneratorTrueTotalPower", + display_name="Generator True Total Power", + value="--", + display_value="0.0 W", + unit="W", + ), + "GeneratorTruePercentOfRatedPower": OncueSensor( + name="GeneratorTruePercentOfRatedPower", + display_name="Generator True Percent Of Rated Power", + value="--", + display_value="0 %", + unit="%", + ), + "GeneratorVoltageAB": OncueSensor( + name="GeneratorVoltageAB", + display_name="Generator Voltage AB", + value="--", + display_value="0.0 V", + unit="V", + ), + "GeneratorVoltageAverageLineToLine": OncueSensor( + name="GeneratorVoltageAverageLineToLine", + display_name="Generator Voltage Average Line To Line", + value="--", + display_value="0.0 V", + unit="V", + ), + "GeneratorCurrentAverage": OncueSensor( + name="GeneratorCurrentAverage", + display_name="Generator Current Average", + value="--", + display_value="0.0 A", + unit="A", + ), + "GeneratorFrequency": OncueSensor( + name="GeneratorFrequency", + display_name="Generator Frequency", + value="--", + display_value="0.0 Hz", + unit="Hz", + ), + "GensetSerialNumber": OncueSensor( + name="GensetSerialNumber", + display_name="Generator Serial Number", + value="--", + display_value="33FDGMFR0026", + unit=None, + ), + "GensetState": OncueSensor( + name="GensetState", + display_name="Generator State", + value="--", + display_value="Off", + unit=None, + ), + "GensetControllerSerialNumber": OncueSensor( + name="GensetControllerSerialNumber", + display_name="Generator Controller Serial Number", + value="--", + display_value="-1", + unit=None, + ), + "GensetModelNumberSelect": OncueSensor( + name="GensetModelNumberSelect", + display_name="Genset Model Number Select", + value="--", + display_value="38 RCLB", + unit=None, + ), + "GensetControllerClockTime": OncueSensor( + name="GensetControllerClockTime", + display_name="Generator Controller Clock Time", + value="--", + display_value="2022-01-13 18:08:13", + unit=None, + ), + "GensetControllerTotalOperationTime": OncueSensor( + name="GensetControllerTotalOperationTime", + display_name="Generator Controller Total Operation Time", + value="--", + display_value="16770.8 h", + unit="h", + ), + "EngineTotalRunTime": OncueSensor( + name="EngineTotalRunTime", + display_name="Engine Total Run Time", + value="--", + display_value="28.1 h", + unit="h", + ), + "EngineTotalRunTimeLoaded": OncueSensor( + name="EngineTotalRunTimeLoaded", + display_name="Engine Total Run Time Loaded", + value="--", + display_value="5.5 h", + unit="h", + ), + "EngineTotalNumberOfStarts": OncueSensor( + name="EngineTotalNumberOfStarts", + display_name="Engine Total Number Of Starts", + value="--", + display_value="101", + unit=None, + ), + "GensetTotalEnergy": OncueSensor( + name="GensetTotalEnergy", + display_name="Genset Total Energy", + value="--", + display_value="1.2022309E7 kWh", + unit="kWh", + ), + "AtsContactorPosition": OncueSensor( + name="AtsContactorPosition", + display_name="Ats Contactor Position", + value="--", + display_value="Source1", + unit=None, + ), + "AtsSourcesAvailable": OncueSensor( + name="AtsSourcesAvailable", + display_name="Ats Sources Available", + value="--", + display_value="Source1", + unit=None, + ), + "Source1VoltageAverageLineToLine": OncueSensor( + name="Source1VoltageAverageLineToLine", + display_name="Source1 Voltage Average Line To Line", + value="--", + display_value="253.5 V", + unit="V", + ), + "Source2VoltageAverageLineToLine": OncueSensor( + name="Source2VoltageAverageLineToLine", + display_name="Source2 Voltage Average Line To Line", + value="--", + display_value="0.0 V", + unit="V", + ), + "IPAddress": OncueSensor( + name="IPAddress", + display_name="IP Address", + value="--", + display_value="1.2.3.4:1026", + unit=None, + ), + "MacAddress": OncueSensor( + name="MacAddress", + display_name="Mac Address", + value="--", + display_value="--", + unit=None, + ), + "ConnectedServerIPAddress": OncueSensor( + name="ConnectedServerIPAddress", + display_name="Connected Server IP Address", + value="--", + display_value="40.117.195.28", + unit=None, + ), + "NetworkConnectionEstablished": OncueSensor( + name="NetworkConnectionEstablished", + display_name="Network Connection Established", + value="--", + display_value="True", + unit=None, + ), + "SerialNumber": OncueSensor( + name="SerialNumber", + display_name="Serial Number", + value="--", + display_value="1073879692", + unit=None, + ), + }, + ) +} + def _patch_login_and_data(): @contextmanager @@ -556,3 +820,28 @@ def _patch_login_and_data_offline_device(): yield return _patcher() + + +def _patch_login_and_data_unavailable(): + @contextmanager + def _patcher(): + with patch("homeassistant.components.oncue.Oncue.async_login"), patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, + ): + yield + + return _patcher() + + +def _patch_login_and_data_unavailable_device(): + @contextmanager + def _patcher(): + + with patch("homeassistant.components.oncue.Oncue.async_login"), patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, + ): + yield + + return _patcher() diff --git a/tests/components/oncue/test_binary_sensor.py b/tests/components/oncue/test_binary_sensor.py index 020b914c76b..f2e7657089f 100644 --- a/tests/components/oncue/test_binary_sensor.py +++ b/tests/components/oncue/test_binary_sensor.py @@ -4,11 +4,11 @@ from __future__ import annotations from homeassistant.components import oncue from homeassistant.components.oncue.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_ON +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from . import _patch_login_and_data +from . import _patch_login_and_data, _patch_login_and_data_unavailable from tests.common import MockConfigEntry @@ -33,3 +33,25 @@ async def test_binary_sensors(hass: HomeAssistant) -> None: ).state == STATE_ON ) + + +async def test_binary_sensors_not_unavailable(hass: HomeAssistant) -> None: + """Test the network connection established binary sensor is available when connection status is false.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "any", CONF_PASSWORD: "any"}, + unique_id="any", + ) + config_entry.add_to_hass(hass) + with _patch_login_and_data_unavailable(): + await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + assert len(hass.states.async_all("binary_sensor")) == 1 + assert ( + hass.states.get( + "binary_sensor.my_generator_network_connection_established" + ).state + == STATE_OFF + ) diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 60c9f68f81b..6319bcdd9f9 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -6,12 +6,17 @@ import pytest from homeassistant.components import oncue from homeassistant.components.oncue.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, 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 -from . import _patch_login_and_data, _patch_login_and_data_offline_device +from . import ( + _patch_login_and_data, + _patch_login_and_data_offline_device, + _patch_login_and_data_unavailable, + _patch_login_and_data_unavailable_device, +) from tests.common import MockConfigEntry @@ -141,3 +146,159 @@ async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: assert ( hass.states.get("sensor.my_generator_generator_current_average").state == "0.0" ) + + +@pytest.mark.parametrize( + "patcher, connections", + [ + [_patch_login_and_data_unavailable_device, set()], + [_patch_login_and_data_unavailable, {("mac", "c9:24:22:6f:14:00")}], + ], +) +async def test_sensors_unavailable(hass: HomeAssistant, patcher, connections) -> None: + """Test that the sensors are unavailable.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "any", CONF_PASSWORD: "any"}, + unique_id="any", + ) + config_entry.add_to_hass(hass) + with patcher(): + await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + assert len(hass.states.async_all("sensor")) == 25 + assert ( + hass.states.get("sensor.my_generator_latest_firmware").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_speed").state == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_oil_pressure").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_coolant_temperature").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_battery_voltage").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_lube_oil_temperature").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_generator_controller_temperature").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_compartment_temperature").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_generator_true_total_power").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get( + "sensor.my_generator_generator_true_percent_of_rated_power" + ).state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get( + "sensor.my_generator_generator_voltage_average_line_to_line" + ).state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_generator_frequency").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_generator_state").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get( + "sensor.my_generator_generator_controller_total_operation_time" + ).state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_total_run_time").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_ats_contactor_position").state + == STATE_UNAVAILABLE + ) + + assert hass.states.get("sensor.my_generator_ip_address").state == STATE_UNAVAILABLE + + assert ( + hass.states.get("sensor.my_generator_connected_server_ip_address").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_target_speed").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_engine_total_run_time_loaded").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get( + "sensor.my_generator_source1_voltage_average_line_to_line" + ).state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get( + "sensor.my_generator_source2_voltage_average_line_to_line" + ).state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_genset_total_energy").state + == STATE_UNAVAILABLE + ) + assert ( + hass.states.get("sensor.my_generator_engine_total_number_of_starts").state + == STATE_UNAVAILABLE + ) + assert ( + hass.states.get("sensor.my_generator_generator_current_average").state + == STATE_UNAVAILABLE + ) + + assert ( + hass.states.get("sensor.my_generator_battery_voltage").state + == STATE_UNAVAILABLE + )