diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt deleted file mode 100644 index f76801b..0000000 --- a/.github/workflows/constraints.txt +++ /dev/null @@ -1,8 +0,0 @@ -pip==25.1.1 -pre-commit==4.2.0 -black==25.1.0 -flake8==7.3.0 -reorder-python-imports==3.15.0 -beautifulsoup4==4.13.4 -dacite==1.9.2 -pipreqs==0.5.0 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c7c7623..ff85b5c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -11,7 +11,7 @@ on: - cron: "0 0 * * *" env: - DEFAULT_PYTHON: 3.11 + DEFAULT_PYTHON: 3.13 jobs: pre-commit: @@ -28,12 +28,12 @@ jobs: - name: Upgrade pip run: | - pip install --constraint=.github/workflows/constraints.txt pip + pip install pip pip --version - name: Install Python modules run: | - pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports pipreqs + pip install pre-commit black flake8 reorder-python-imports pipreqs - name: Run pre-commit on all files run: | @@ -61,3 +61,28 @@ jobs: - name: Hassfest validation uses: "home-assistant/actions/hassfest@master" + + pytest: + runs-on: "ubuntu-latest" + name: Pytest + steps: + - name: Check out the repository + uses: actions/checkout@v4.2.2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v5.6.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Upgrade pip + run: | + pip install pip + pip --version + + - name: Install dependencies + run: | + pip install -r requirements.txt + + - name: Run pytest + run: | + pytest --maxfail=3 --disable-warnings --color=yes diff --git a/local_test.py b/_test_local_access.py similarity index 100% rename from local_test.py rename to _test_local_access.py diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index 5d109cb..1cc0317 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -19,7 +19,6 @@ from .coordinator import GeneracDataUpdateCoordinator from .utils import async_client_session - _LOGGER: logging.Logger = logging.getLogger(__package__) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..22d38ce --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +asyncio_mode = auto +markers = + socket: Enables socket connections diff --git a/requirements.txt b/requirements.txt index 7df4260..ce6a2eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,13 @@ beautifulsoup4==4.13.4 dacite==1.9.2 -homeassistant==2025.3.3 +homeassistant==2025.7.3 voluptuous==0.15.2 pre-commit==4.2.0 reorder-python-imports==3.15.0 +pytest==8.4.0 +pytest-homeassistant-custom-component==0.13.263 +pytest-asyncio==1.0.0 +pytest-socket==0.7.0 +respx==0.22.0 +aioresponses==0.7.8 Brotli==1.1.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..b91b51c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the Generac integration.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..806c0c3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +"""Global fixtures for generac integration.""" +import pytest + +pytest_plugins = "pytest_homeassistant_custom_component" + + +@pytest.fixture(autouse=True) +def auto_enable_custom_integrations(enable_custom_integrations): + """Enable custom integrations defined in the test dir.""" + yield diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..82c146d --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,86 @@ +"""Test the Generac API.""" +import re + +import aiohttp +import pytest +from aioresponses import aioresponses +from custom_components.generac.api import GeneracApiClient +from custom_components.generac.api import get_setting_json + + +def test_get_setting_json(): + """Test the get_setting_json function.""" + html = """ +
+ + + + +""" + assert get_setting_json(html) == {"key": "value"} + + +def test_get_setting_json_no_settings(): + """Test the get_setting_json function when there are no settings.""" + html = """ + + + + +""" + assert get_setting_json(html) is None + + +@pytest.mark.asyncio +async def test_api_flow(): + """Test the full API flow.""" + with aioresponses() as m: + m.get( + "https://app.mobilelinkgen.com/api/Auth/SignIn?email=test-username", + status=200, + body="""""", + ) + m.post( + re.compile( + r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/SelfAsserted.*" + ), + status=200, + payload={"status": "200"}, + ) + m.get( + re.compile( + r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/api/CombinedSigninAndSignup/confirmed.*" + ), + status=200, + body="""""", + ) + m.post("https://app.mobilelinkgen.com/test-action", status=200) + m.get( + "https://app.mobilelinkgen.com/api/v2/Apparatus/list", + status=200, + payload=[ + { + "apparatusId": 12345, + "type": 0, + "name": "test-name", + } + ], + ) + m.get( + "https://app.mobilelinkgen.com/api/v1/Apparatus/details/12345", + status=200, + payload={"key": "value"}, + ) + + async with aiohttp.ClientSession() as session: + client = GeneracApiClient("test-username", "test-password", session) + data = await client.async_get_data() + assert data is not None + assert data["12345"].apparatus.apparatusId == 12345 + # This is a bit of a hack, but we don't have the ApparatusDetail model fully defined for this test + # assert data["12345"].detail.raw == {"key": "value"} + assert client.csrf == "test-csrf" diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py new file mode 100644 index 0000000..f4dcd5c --- /dev/null +++ b/tests/test_binary_sensor.py @@ -0,0 +1,100 @@ +"""Test the Generac binary sensor platform.""" +from unittest.mock import MagicMock + +from custom_components.generac.binary_sensor import GeneracConnectedSensor +from custom_components.generac.binary_sensor import GeneracConnectingSensor +from custom_components.generac.binary_sensor import GeneracMaintenanceAlertSensor +from custom_components.generac.binary_sensor import GeneracWarningSensor +from custom_components.generac.models import Apparatus +from custom_components.generac.models import ApparatusDetail +from custom_components.generac.models import Item + + +def get_mock_item( + is_connected: bool, + is_connecting: bool, + has_maintenance_alert: bool, + show_warning: bool, +) -> Item: + """Return a mock Item object.""" + return Item( + apparatus=Apparatus(), + apparatusDetail=ApparatusDetail( + isConnected=is_connected, + isConnecting=is_connecting, + hasMaintenanceAlert=has_maintenance_alert, + showWarning=show_warning, + ), + ) + + +async def test_connected_sensor(hass): + """Test the connected sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test when connected + item = get_mock_item(True, False, False, False) + sensor = GeneracConnectedSensor(coordinator, entry, "12345", item) + assert sensor.is_on is True + assert sensor.name == "generac_12345_is_connected" + assert sensor.device_class == "connectivity" + + # Test when not connected + item = get_mock_item(False, False, False, False) + sensor = GeneracConnectedSensor(coordinator, entry, "12345", item) + assert sensor.is_on is False + + +async def test_connecting_sensor(hass): + """Test the connecting sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test when connecting + item = get_mock_item(False, True, False, False) + sensor = GeneracConnectingSensor(coordinator, entry, "12345", item) + assert sensor.is_on is True + assert sensor.name == "generac_12345_is_connecting" + assert sensor.device_class == "connectivity" + + # Test when not connecting + item = get_mock_item(False, False, False, False) + sensor = GeneracConnectingSensor(coordinator, entry, "12345", item) + assert sensor.is_on is False + + +async def test_maintenance_alert_sensor(hass): + """Test the maintenance alert sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test when maintenance alert is active + item = get_mock_item(False, False, True, False) + sensor = GeneracMaintenanceAlertSensor(coordinator, entry, "12345", item) + assert sensor.is_on is True + assert sensor.name == "generac_12345_has_maintenance_alert" + assert sensor.device_class == "safety" + + # Test when maintenance alert is not active + item = get_mock_item(False, False, False, False) + sensor = GeneracMaintenanceAlertSensor(coordinator, entry, "12345", item) + assert sensor.is_on is False + + +async def test_warning_sensor(hass): + """Test the warning sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test when warning is active + item = get_mock_item(False, False, False, True) + sensor = GeneracWarningSensor(coordinator, entry, "12345", item) + assert sensor.is_on is True + assert sensor.name == "generac_12345_show_warning" + assert sensor.device_class == "safety" + + # Test when warning is not active + item = get_mock_item(False, False, False, False) + sensor = GeneracWarningSensor(coordinator, entry, "12345", item) + assert sensor.is_on is False diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 0000000..3d20bd7 --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,41 @@ +"""Test the Generac config flow.""" +from unittest.mock import patch + +from custom_components.generac.const import DOMAIN +from homeassistant import config_entries +from homeassistant import setup +from homeassistant.core import HomeAssistant + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "custom_components.generac.config_flow.GeneracApiClient.async_get_data", + return_value=True, + ), patch( + "custom_components.generac.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py new file mode 100644 index 0000000..5ea2c59 --- /dev/null +++ b/tests/test_coordinator.py @@ -0,0 +1,43 @@ +"""Test the Generac data update coordinator.""" +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest +from custom_components.generac.coordinator import GeneracDataUpdateCoordinator +from homeassistant.helpers.update_coordinator import UpdateFailed + + +async def test_coordinator_init(hass): + """Test the coordinator initialization.""" + config_entry = MagicMock() + config_entry.options = {} + client = MagicMock() + coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry) + assert coordinator.hass is hass + assert coordinator.api is client + assert coordinator._config_entry is config_entry + assert not coordinator.is_online + + +async def test_coordinator_update_data(hass): + """Test the coordinator update data.""" + config_entry = MagicMock() + config_entry.options = {} + client = MagicMock() + client.async_get_data = AsyncMock(return_value={"foo": "bar"}) + coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry) + coordinator.data = await coordinator._async_update_data() + assert coordinator.data == {"foo": "bar"} + assert coordinator.is_online + + +async def test_coordinator_update_data_fails(hass): + """Test the coordinator update data fails.""" + config_entry = MagicMock() + config_entry.options = {} + client = MagicMock() + client.async_get_data = AsyncMock(side_effect=Exception) + coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry) + with pytest.raises(UpdateFailed): + await coordinator._async_update_data() + assert not coordinator.is_online diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 0000000..9893e44 --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,34 @@ +"""Test the Generac diagnostics.""" +from unittest.mock import MagicMock + +from custom_components.generac.diagnostics import async_get_config_entry_diagnostics +from custom_components.generac.models import Apparatus +from custom_components.generac.models import ApparatusDetail +from custom_components.generac.models import Item + + +async def test_diagnostics(hass): + """Test the diagnostics.""" + coordinator = MagicMock() + coordinator.data = { + "12345": Item( + apparatus=Apparatus( + serialNumber="1234567890", + apparatusId=12345, + localizedAddress="123 Main St", + ), + apparatusDetail=ApparatusDetail( + deviceSsid="TestSSID", + ), + ) + } + entry = MagicMock() + entry.entry_id = "test_entry_id" + hass.data = {"generac": {"test_entry_id": coordinator}} + + diagnostics = await async_get_config_entry_diagnostics(hass, entry) + + assert diagnostics["data"]["12345"]["apparatus"]["serialNumber"] == "REDACTED" + assert diagnostics["data"]["12345"]["apparatus"]["apparatusId"] == "REDACTED" + assert diagnostics["data"]["12345"]["apparatus"]["localizedAddress"] == "REDACTED" + assert diagnostics["data"]["12345"]["apparatusDetail"]["deviceSsid"] == "REDACTED" diff --git a/tests/test_entity.py b/tests/test_entity.py new file mode 100644 index 0000000..59a9e88 --- /dev/null +++ b/tests/test_entity.py @@ -0,0 +1,55 @@ +"""Test the Generac entity.""" +from unittest.mock import MagicMock + +from custom_components.generac.entity import GeneracEntity +from custom_components.generac.models import Apparatus +from custom_components.generac.models import ApparatusDetail +from custom_components.generac.models import Item + + +def get_mock_item() -> Item: + """Return a mock Item object.""" + return Item( + apparatus=Apparatus( + name="Test Generator", + modelNumber="G12345", + ), + apparatusDetail=ApparatusDetail(), + ) + + +async def test_entity(hass): + """Test the entity.""" + coordinator = MagicMock() + entry = MagicMock() + entry.entry_id = "test_entry_id" + item = get_mock_item() + entity = GeneracEntity(coordinator, entry, "12345", item) + entity.hass = hass + entity.async_write_ha_state = MagicMock() + + assert entity.device_info == { + "identifiers": {("generac", "12345")}, + "name": "Test Generator", + "model": "G12345", + "manufacturer": "Generac", + } + assert entity.device_state_attributes == { + "attribution": "Data provided by https://app.mobilelinkgen.com/api. This is reversed engineered. Heavily inspired by https://github.com/digitaldan/openhab-addons/blob/generac-2.0/bundles/org.openhab.binding.generacmobilelink/README.md", + "id": "12345", + "integration": "generac", + } + assert entity.available is True + + # Test coordinator update + new_item = Item( + apparatus=Apparatus( + name="New Test Generator", + modelNumber="G67890", + ), + apparatusDetail=ApparatusDetail(), + ) + coordinator.data = {"12345": new_item} + entity._handle_coordinator_update() + assert entity.item == new_item + assert entity.async_write_ha_state.called diff --git a/tests/test_image.py b/tests/test_image.py new file mode 100644 index 0000000..c3a712b --- /dev/null +++ b/tests/test_image.py @@ -0,0 +1,49 @@ +"""Test the Generac image platform.""" +from unittest.mock import AsyncMock +from unittest.mock import MagicMock +from unittest.mock import patch + +import httpx +from custom_components.generac.image import HeroImageSensor +from custom_components.generac.models import Apparatus +from custom_components.generac.models import ApparatusDetail +from custom_components.generac.models import Item + + +def get_mock_item(hero_image_url: str) -> Item: + """Return a mock Item object.""" + return Item( + apparatus=Apparatus(), + apparatusDetail=ApparatusDetail(heroImageUrl=hero_image_url), + ) + + +async def test_image_sensor(hass): + """Test the image sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test with a valid image URL + item = get_mock_item("http://example.com/image.png") + sensor = HeroImageSensor(coordinator, entry, "12345", item, hass) + assert sensor.image_url == "http://example.com/image.png" + assert sensor.name == "generac_12345_hero_image" + assert sensor.available is True + + # Test with no image URL + item = get_mock_item(None) + sensor = HeroImageSensor(coordinator, entry, "12345", item, hass) + assert sensor.image_url is None + assert sensor.available is False + + # Test _fetch_url + item = get_mock_item("http://example.com/image.jpg") + sensor = HeroImageSensor(coordinator, entry, "12345", item, hass) + response = httpx.Response(200, headers={"content-type": "text/plain"}) + with patch( + "homeassistant.components.image.ImageEntity._fetch_url", + new_callable=AsyncMock, + return_value=response, + ): + result = await sensor._fetch_url("http://example.com/image.jpg") + assert result.headers["content-type"] == "image/jpeg" diff --git a/tests/test_sensor.py b/tests/test_sensor.py new file mode 100644 index 0000000..8fa775e --- /dev/null +++ b/tests/test_sensor.py @@ -0,0 +1,269 @@ +"""Test the Generac sensor platform.""" +from unittest.mock import MagicMock + +from custom_components.generac.const import DEVICE_TYPE_GENERATOR +from custom_components.generac.const import DEVICE_TYPE_PROPANE_MONITOR +from custom_components.generac.models import Apparatus +from custom_components.generac.models import ApparatusDetail +from custom_components.generac.models import Item +from custom_components.generac.models import Weather +from custom_components.generac.sensor import ActivationDateSensor +from custom_components.generac.sensor import AddressSensor +from custom_components.generac.sensor import BatteryLevelSensor +from custom_components.generac.sensor import BatteryVoltageSensor +from custom_components.generac.sensor import CapacitySensor +from custom_components.generac.sensor import ConnectionTimeSensor +from custom_components.generac.sensor import DealerEmailSensor +from custom_components.generac.sensor import DealerNameSensor +from custom_components.generac.sensor import DealerPhoneSensor +from custom_components.generac.sensor import DeviceSsidSensor +from custom_components.generac.sensor import DeviceTypeSensor +from custom_components.generac.sensor import FuelLevelSensor +from custom_components.generac.sensor import FuelTypeSensor +from custom_components.generac.sensor import LastReadingDateSensor +from custom_components.generac.sensor import LastSeenSensor +from custom_components.generac.sensor import ModelNumberSensor +from custom_components.generac.sensor import OrientationSensor +from custom_components.generac.sensor import OutdoorTemperatureSensor +from custom_components.generac.sensor import PanelIDSensor +from custom_components.generac.sensor import ProtectionTimeSensor +from custom_components.generac.sensor import RunTimeSensor +from custom_components.generac.sensor import SerialNumberSensor +from custom_components.generac.sensor import SignalStrengthSensor +from custom_components.generac.sensor import StatusLabelSensor +from custom_components.generac.sensor import StatusSensor +from custom_components.generac.sensor import StatusTextSensor + + +def get_mock_item( + device_type: int, + status: int, + prop_status: str = None, + run_time: int = 0, + protection_time: int = 0, + activation_date: str = None, + last_seen: str = None, + connection_time: str = None, + battery_voltage: float = 0.0, + device_type_str: str = None, + dealer_email: str = None, + dealer_name: str = None, + dealer_phone: str = None, + address: str = None, + status_text: str = None, + status_label: str = None, + serial_number: str = None, + model_number: str = None, + device_ssid: str = None, + panel_id: str = None, + signal_strength: str = None, + capacity: int = 0, + fuel_level: int = 0, + fuel_type: str = None, + orientation: str = None, + last_reading_date: str = None, + battery_level: int = 0, + outdoor_temperature: float = None, + outdoor_temperature_unit: str = None, + outdoor_temperature_unit_type: int = None, + weather_icon_code: int = None, +) -> Item: + """Return a mock Item object.""" + return Item( + apparatus=Apparatus( + type=device_type, + serialNumber=serial_number, + modelNumber=model_number, + panelId=panel_id, + localizedAddress=address, + preferredDealerName=dealer_name, + preferredDealerEmail=dealer_email, + preferredDealerPhone=dealer_phone, + ), + apparatusDetail=ApparatusDetail( + apparatusStatus=status, + properties=[ + MagicMock(type=70, value=run_time), + MagicMock(type=31, value=protection_time), + MagicMock(type=69, value=battery_voltage), + ], + activationDate=activation_date, + lastSeen=last_seen, + connectionTimestamp=connection_time, + deviceType=device_type_str, + statusText=status_text, + statusLabel=status_label, + deviceSsid=device_ssid, + tuProperties=[ + MagicMock(type=1, value=capacity), + MagicMock(type=9, value=fuel_level), + MagicMock(type=0, value=fuel_type), + MagicMock(type=2, value=orientation), + MagicMock(type=11, value=last_reading_date), + MagicMock(type=17, value=battery_level), + ], + weather=Weather( + temperature=Weather.Temperature( + value=outdoor_temperature, + unit=outdoor_temperature_unit, + unitType=outdoor_temperature_unit_type, + ), + iconCode=weather_icon_code, + ) + if outdoor_temperature is not None + else None, + ), + ) + + +async def test_status_sensor(hass): + """Test the status sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test generator status + item = get_mock_item(DEVICE_TYPE_GENERATOR, 1) + sensor = StatusSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Ready" + assert sensor.name == "generac_12345_status" + assert sensor.device_class == "enum" + assert sensor.icon == "mdi:power" + + # Test propane monitor status + item = get_mock_item(DEVICE_TYPE_PROPANE_MONITOR, 1, "Online") + item.apparatus.properties = [MagicMock(type=3, value=MagicMock(status="Online"))] + sensor = StatusSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Online" + + +async def test_generator_sensors(hass): + """Test the generator sensors.""" + coordinator = MagicMock() + entry = MagicMock() + item = get_mock_item( + DEVICE_TYPE_GENERATOR, + 1, + run_time=123, + protection_time=456, + activation_date="2022-01-01T00:00:00Z", + last_seen="2022-01-02T00:00:00Z", + connection_time="2022-01-03T00:00:00Z", + battery_voltage=12.3, + device_type_str="wifi", + dealer_email="test@example.com", + dealer_name="Test Dealer", + dealer_phone="123-456-7890", + address="123 Main St", + status_text="Ready", + status_label="Ready", + serial_number="1234567890", + model_number="G12345", + device_ssid="TestSSID", + panel_id="P12345", + signal_strength="100%", + outdoor_temperature=72.0, + outdoor_temperature_unit="F", + ) + item.apparatus.properties = [ + MagicMock(type=3, value=MagicMock(signalStrength="100%")) + ] + + sensor = RunTimeSensor(coordinator, entry, "12345", item) + assert sensor.native_value == 123 + + sensor = ProtectionTimeSensor(coordinator, entry, "12345", item) + assert sensor.native_value == 456 + + sensor = ActivationDateSensor(coordinator, entry, "12345", item) + assert sensor.native_value.isoformat() == "2022-01-01T00:00:00+00:00" + + sensor = LastSeenSensor(coordinator, entry, "12345", item) + assert sensor.native_value.isoformat() == "2022-01-02T00:00:00+00:00" + + sensor = ConnectionTimeSensor(coordinator, entry, "12345", item) + assert sensor.native_value.isoformat() == "2022-01-03T00:00:00+00:00" + + sensor = BatteryVoltageSensor(coordinator, entry, "12345", item) + assert sensor.native_value == 12.3 + + sensor = DeviceTypeSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Wifi" + + sensor = DealerEmailSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "test@example.com" + + sensor = DealerNameSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Test Dealer" + + sensor = DealerPhoneSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "123-456-7890" + + sensor = AddressSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "123 Main St" + + sensor = StatusTextSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Ready" + + sensor = StatusLabelSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Ready" + + sensor = SerialNumberSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "1234567890" + + sensor = ModelNumberSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "G12345" + + sensor = DeviceSsidSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "TestSSID" + + sensor = PanelIDSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "P12345" + + sensor = SignalStrengthSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "100%" + + sensor = OutdoorTemperatureSensor(coordinator, entry, "12345", item) + assert sensor.native_value == 72.0 + assert sensor.native_unit_of_measurement == "°F" + + +async def test_propane_monitor_sensors(hass): + """Test the propane monitor sensors.""" + coordinator = MagicMock() + entry = MagicMock() + item = get_mock_item( + DEVICE_TYPE_PROPANE_MONITOR, + 1, + capacity=100, + fuel_level=50, + fuel_type="Propane", + orientation="Vertical", + last_reading_date="2022-01-01T00:00:00Z", + battery_level=75, + address="456 Oak Ave", + device_type_str="lte-tankutility-v2", + ) + + sensor = CapacitySensor(coordinator, entry, "12345", item) + assert sensor.native_value == 100 + + sensor = FuelLevelSensor(coordinator, entry, "12345", item) + assert sensor.native_value == 50 + + sensor = FuelTypeSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Propane" + + sensor = OrientationSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "Vertical" + + sensor = LastReadingDateSensor(coordinator, entry, "12345", item) + assert sensor.native_value.isoformat() == "2022-01-01T00:00:00+00:00" + + sensor = BatteryLevelSensor(coordinator, entry, "12345", item) + assert sensor.native_value == 75 + + sensor = AddressSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "456 Oak Ave" + + sensor = DeviceTypeSensor(coordinator, entry, "12345", item) + assert sensor.native_value == "lte-tankutility-v2" diff --git a/tests/test_weather.py b/tests/test_weather.py new file mode 100644 index 0000000..a9e66de --- /dev/null +++ b/tests/test_weather.py @@ -0,0 +1,66 @@ +"""Test the Generac weather platform.""" +from unittest.mock import MagicMock + +from custom_components.generac.models import Apparatus +from custom_components.generac.models import ApparatusDetail +from custom_components.generac.models import Item +from custom_components.generac.models import Weather +from custom_components.generac.weather import WeatherSensor + + +def get_mock_item(icon_code: int, temperature: float, temperature_unit: str) -> Item: + """Return a mock Item object.""" + return Item( + apparatus=Apparatus( + weather=Weather( + iconCode=icon_code, + temperature=Weather.Temperature( + value=temperature, unit=temperature_unit, unitType=1 + ), + ) + ), + apparatusDetail=ApparatusDetail( + weather=Weather( + iconCode=icon_code, + temperature=Weather.Temperature( + value=temperature, unit=temperature_unit, unitType=1 + ), + ) + ), + ) + + +async def test_weather_sensor(hass): + """Test the weather sensor.""" + coordinator = MagicMock() + entry = MagicMock() + + # Test sunny condition + item = get_mock_item(1, 72.0, "F") + sensor = WeatherSensor(coordinator, entry, "12345", item) + assert sensor.condition == "sunny" + assert sensor.native_temperature == 72.0 + assert sensor.native_temperature_unit == "°F" + assert sensor.name == "generac_12345_weather" + + # Test cloudy condition + item = get_mock_item(7, 65.0, "C") + sensor = WeatherSensor(coordinator, entry, "12345", item) + assert sensor.condition == "cloudy" + assert sensor.native_temperature == 65.0 + assert sensor.native_temperature_unit == "°C" + + # Test rainy condition + item = get_mock_item(12, 50.0, "F") + sensor = WeatherSensor(coordinator, entry, "12345", item) + assert sensor.condition == "rainy" + + # Test snowy condition + item = get_mock_item(19, 30.0, "F") + sensor = WeatherSensor(coordinator, entry, "12345", item) + assert sensor.condition == "snowy" + + # Test exceptional condition + item = get_mock_item(99, 70.0, "F") + sensor = WeatherSensor(coordinator, entry, "12345", item) + assert sensor.condition == "exceptional"