From 168283bd66a9ca683e96db17d51bb7a05bbbb527 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:06:40 -0400 Subject: [PATCH 01/31] fix(tests): fix broken api tests --- pytest.ini | 4 +++ requirements.txt | 15 ++++----- tests/test_api.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/test_api.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b4546f2 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +asyncio_mode = auto +markers = + socket: Enables socket connections \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7df4260..1955947 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -beautifulsoup4==4.13.4 -dacite==1.9.2 -homeassistant==2025.3.3 -voluptuous==0.15.2 -pre-commit==4.2.0 -reorder-python-imports==3.15.0 -Brotli==1.1.0 +aiohttp +beautifulsoup4 +dacite +pytest +pytest-asyncio +pytest-socket +respx +aioresponses diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..7e77084 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,78 @@ +"""Test the Generac API.""" +import re +import aiohttp +import pytest +from aioresponses import aioresponses + +from custom_components.generac.api import GeneracApiClient, 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" \ No newline at end of file From b748619327e00c11a51a9cec02d6549804ec38a4 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:09:47 -0400 Subject: [PATCH 02/31] Set up basic tests and moved local_test for the API call into a proper test we can optionally run. --- tests/__init__.py | 1 + tests/conftest.py | 9 ++++++ tests/test_config_flow.py | 41 ++++++++++++++++++++++++++++ local_test.py => tests/test_local.py | 0 4 files changed, 51 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_config_flow.py rename local_test.py => tests/test_local.py (100%) 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..5ddbe04 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +"""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_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 0000000..9a86d74 --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,41 @@ +"""Test the Generac config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.core import HomeAssistant + +from custom_components.generac.const import DOMAIN + + +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/local_test.py b/tests/test_local.py similarity index 100% rename from local_test.py rename to tests/test_local.py From 51dcb8bc32f7aab49f4721afdcd0d18740ac2d30 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:13:33 -0400 Subject: [PATCH 03/31] feat(tests): add tests for coordinator --- tests/test_coordinator.py | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/test_coordinator.py diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py new file mode 100644 index 0000000..2d1721d --- /dev/null +++ b/tests/test_coordinator.py @@ -0,0 +1,44 @@ +"""Test the Generac data update coordinator.""" +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import pytest +from homeassistant.helpers.update_coordinator import UpdateFailed + +from custom_components.generac.coordinator import GeneracDataUpdateCoordinator + + +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 \ No newline at end of file From 179873792b4fa7b59e700fb55fa8bd6afe4779c8 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:16:27 -0400 Subject: [PATCH 04/31] feat(tests): add tests for binary sensors --- tests/test_binary_sensor.py | 95 +++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tests/test_binary_sensor.py diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py new file mode 100644 index 0000000..9abcc1b --- /dev/null +++ b/tests/test_binary_sensor.py @@ -0,0 +1,95 @@ +"""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 Item, Apparatus, ApparatusDetail + + +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 \ No newline at end of file From 25cb0feeca9d9327281c9ebc7ff5a617652c276e Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:20:38 -0400 Subject: [PATCH 05/31] feat(tests): add tests for sensors --- tests/test_sensor.py | 266 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 tests/test_sensor.py diff --git a/tests/test_sensor.py b/tests/test_sensor.py new file mode 100644 index 0000000..0f4b6b5 --- /dev/null +++ b/tests/test_sensor.py @@ -0,0 +1,266 @@ +"""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 Item, Apparatus, ApparatusDetail, Weather +from custom_components.generac.sensor import ( + StatusSensor, + RunTimeSensor, + ProtectionTimeSensor, + ActivationDateSensor, + LastSeenSensor, + ConnectionTimeSensor, + BatteryVoltageSensor, + DeviceTypeSensor, + DealerEmailSensor, + DealerNameSensor, + DealerPhoneSensor, + AddressSensor, + StatusTextSensor, + StatusLabelSensor, + SerialNumberSensor, + ModelNumberSensor, + DeviceSsidSensor, + PanelIDSensor, + SignalStrengthSensor, + CapacitySensor, + FuelLevelSensor, + FuelTypeSensor, + OrientationSensor, + LastReadingDateSensor, + BatteryLevelSensor, + OutdoorTemperatureSensor, +) + + +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" \ No newline at end of file From 463ce92a9d12757365bf867474e40d191dd6f0d9 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:39:25 -0400 Subject: [PATCH 06/31] Add test coverage for weather.py --- tests/test_weather.py | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_weather.py diff --git a/tests/test_weather.py b/tests/test_weather.py new file mode 100644 index 0000000..02b292a --- /dev/null +++ b/tests/test_weather.py @@ -0,0 +1,64 @@ +"""Test the Generac weather platform.""" + +from unittest.mock import MagicMock + +from custom_components.generac.models import Item, Apparatus, ApparatusDetail, 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" From d488a4f1946721d279a9b56344f9f0317b760483 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 00:40:07 -0400 Subject: [PATCH 07/31] feat(tests): add tests for diagnostics and entity --- tests/test_diagnostics.py | 32 +++++++++++++++++++++++ tests/test_entity.py | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 tests/test_diagnostics.py create mode 100644 tests/test_entity.py diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py new file mode 100644 index 0000000..d4cbe4d --- /dev/null +++ b/tests/test_diagnostics.py @@ -0,0 +1,32 @@ +"""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 Item, Apparatus, ApparatusDetail + + +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..69ce34c --- /dev/null +++ b/tests/test_entity.py @@ -0,0 +1,53 @@ +"""Test the Generac entity.""" +from unittest.mock import MagicMock + +from custom_components.generac.entity import GeneracEntity +from custom_components.generac.models import Item, Apparatus, ApparatusDetail + + +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 \ No newline at end of file From 295b081c5c55939485a5e63d381fc9b3a11538a9 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 00:45:59 -0400 Subject: [PATCH 08/31] feat(tests): add tests for image --- tests/test_image.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/test_image.py diff --git a/tests/test_image.py b/tests/test_image.py new file mode 100644 index 0000000..1591014 --- /dev/null +++ b/tests/test_image.py @@ -0,0 +1,46 @@ +"""Test the Generac image platform.""" +from unittest.mock import MagicMock, AsyncMock, patch + +import httpx + +from custom_components.generac.image import HeroImageSensor +from custom_components.generac.models import Item, Apparatus, ApparatusDetail + + +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" From 8199abab70435227554e4d699f8a89e390adfd09 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 01:51:00 -0400 Subject: [PATCH 09/31] Fix pip package versions --- requirements.txt | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1955947..1aa27b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,12 @@ -aiohttp -beautifulsoup4 -dacite -pytest -pytest-asyncio -pytest-socket -respx -aioresponses +aiohttp==3.12.14 +beautifulsoup4==4.13.4 +dacite==1.9.2 +homeassistant==2025.2.2 +voluptuous==0.15.2 +pre-commit==4.1.0 +reorder-python-imports==3.14.0 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-socket==0.7.0 +respx==0.22.0 +aioresponses==0.7.8 \ No newline at end of file From 0b20814aff49ee974faed35525c9cce94d195563 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 01:51:19 -0400 Subject: [PATCH 10/31] Move local test back to root directory since it's not meant to be a unit test --- tests/test_local.py => test_local_access.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/test_local.py => test_local_access.py (100%) diff --git a/tests/test_local.py b/test_local_access.py similarity index 100% rename from tests/test_local.py rename to test_local_access.py From 2f7caaa09aa1ea49ea755e1f273144ad43331cdc Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 01:53:39 -0400 Subject: [PATCH 11/31] Update homeassistant package version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1aa27b1..a2809b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ aiohttp==3.12.14 beautifulsoup4==4.13.4 dacite==1.9.2 -homeassistant==2025.2.2 +homeassistant==2025.7.2 voluptuous==0.15.2 pre-commit==4.1.0 reorder-python-imports==3.14.0 From e2285cd8154b4449eba9da22614690c1024ae8a9 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 02:23:53 -0400 Subject: [PATCH 12/31] Prevent local access test from being used by pytest --- test_local_access.py => _test_local_access.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test_local_access.py => _test_local_access.py (100%) diff --git a/test_local_access.py b/_test_local_access.py similarity index 100% rename from test_local_access.py rename to _test_local_access.py From 15291168d2a477060e185c6262a20096b7f30d01 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 02:24:58 -0400 Subject: [PATCH 13/31] Restore missing newline for requirements file --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a2809b2..a6616da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,4 @@ pytest==8.4.1 pytest-asyncio==1.1.0 pytest-socket==0.7.0 respx==0.22.0 -aioresponses==0.7.8 \ No newline at end of file +aioresponses==0.7.8 From d27074eaf79dc45f721cdfb53b990771ffec9145 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 07:49:34 +0000 Subject: [PATCH 14/31] Bump pre-commit from 4.1.0 to 4.2.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v4.1.0...v4.2.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6616da..e56425e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ beautifulsoup4==4.13.4 dacite==1.9.2 homeassistant==2025.7.2 voluptuous==0.15.2 -pre-commit==4.1.0 +pre-commit==4.2.0 reorder-python-imports==3.14.0 pytest==8.4.1 pytest-asyncio==1.1.0 From 687ce6ec4aaa2bca521302ccc51b2c5f16584a9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 07:43:03 +0000 Subject: [PATCH 15/31] Bump reorder-python-imports from 3.14.0 to 3.15.0 Bumps [reorder-python-imports](https://github.com/asottile/reorder-python-imports) from 3.14.0 to 3.15.0. - [Commits](https://github.com/asottile/reorder-python-imports/compare/v3.14.0...v3.15.0) --- updated-dependencies: - dependency-name: reorder-python-imports dependency-version: 3.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e56425e..33dd55e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ dacite==1.9.2 homeassistant==2025.7.2 voluptuous==0.15.2 pre-commit==4.2.0 -reorder-python-imports==3.14.0 +reorder-python-imports==3.15.0 pytest==8.4.1 pytest-asyncio==1.1.0 pytest-socket==0.7.0 From a988a876b43c2f01f470ad41668bd9051ba8da26 Mon Sep 17 00:00:00 2001 From: Phu Nguyen Date: Thu, 19 Jun 2025 11:45:19 -0500 Subject: [PATCH 16/31] added helper for aiohttp to return clientsession with quote_cookie=False refactored local_test.py for python 3.10+ added Brotli requirement for compression format removed aiohttp as requirement to use homeassistant to provide correct version --- _test_local_access.py | 3 +-- custom_components/generac/__init__.py | 1 + custom_components/generac/utils.py | 1 + requirements.txt | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/_test_local_access.py b/_test_local_access.py index 9cc715a..4f08a83 100644 --- a/_test_local_access.py +++ b/_test_local_access.py @@ -13,8 +13,7 @@ import json import logging import os - -import aiohttp # type: ignore +import aiohttp from custom_components.generac.api import GeneracApiClient logging.basicConfig(level=logging.DEBUG) diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index 5d109cb..290efc7 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -11,6 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .api import GeneracApiClient +from .utilts import async_client_session from .const import CONF_PASSWORD from .const import CONF_USERNAME from .const import DOMAIN diff --git a/custom_components/generac/utils.py b/custom_components/generac/utils.py index 4b48267..7eef4a2 100644 --- a/custom_components/generac/utils.py +++ b/custom_components/generac/utils.py @@ -1,4 +1,5 @@ """Helper to create an aiohttp client session for the Generac API.""" + from aiohttp import ClientSession from aiohttp import CookieJar from homeassistant.core import HomeAssistant diff --git a/requirements.txt b/requirements.txt index 33dd55e..5294c78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pytest-asyncio==1.1.0 pytest-socket==0.7.0 respx==0.22.0 aioresponses==0.7.8 +Brotli==1.1.0 From 9b92487b27b478f07ae1b8716469237046f2953e Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Thu, 19 Jun 2025 18:04:11 +0000 Subject: [PATCH 17/31] Fix linting errors --- _test_local_access.py | 3 ++- custom_components/generac/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/_test_local_access.py b/_test_local_access.py index 4f08a83..9cc715a 100644 --- a/_test_local_access.py +++ b/_test_local_access.py @@ -13,7 +13,8 @@ import json import logging import os -import aiohttp + +import aiohttp # type: ignore from custom_components.generac.api import GeneracApiClient logging.basicConfig(level=logging.DEBUG) diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index 290efc7..8390078 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -4,6 +4,7 @@ For more details about this integration, please refer to https://github.com/binarydev/generac """ + import logging from homeassistant.config_entries import ConfigEntry @@ -11,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .api import GeneracApiClient -from .utilts import async_client_session +from .utils import async_client_session from .const import CONF_PASSWORD from .const import CONF_USERNAME from .const import DOMAIN From 1d127864ab6bb69afcbcec52f26d6d81438e2bd4 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Mon, 14 Jul 2025 01:09:47 -0400 Subject: [PATCH 18/31] Set up basic tests and moved local_test for the API call into a proper test we can optionally run. --- tests/test_local.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test_local.py diff --git a/tests/test_local.py b/tests/test_local.py new file mode 100644 index 0000000..9cc715a --- /dev/null +++ b/tests/test_local.py @@ -0,0 +1,45 @@ +# This script is a standalone test for the Generac API client. +# It logs in to the Generac API using credentials from environment variables +# and prints the device data in JSON format using a custom JSON encoder. +# Make sure to set the environment variables GENERAC_USER and GENERAC_PASS before running this script +# to avoid hardcoding sensitive information in the script. +# This script is intended to be run outside of Home Assistant, for testing purposes. +# It uses the aiohttp library for asynchronous HTTP requests and custom JSON encoding for dataclasses. +# Ensure you have aiohttp installed in your environment. +# If you're using Home Assistant, aiohttp is already included. +# You can install it using pip: pip install aiohttp +import asyncio +import dataclasses +import json +import logging +import os + +import aiohttp # type: ignore +from custom_components.generac.api import GeneracApiClient + +logging.basicConfig(level=logging.DEBUG) + + +# Your custom JSON encoder +class EnhancedJSONEncoder(json.JSONEncoder): + def default(self, o): + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + return super().default(o) + + +# Async main logic +async def main(): + jar = aiohttp.CookieJar(unsafe=True, quote_cookie=False) + async with aiohttp.ClientSession(cookie_jar=jar) as session: + api = GeneracApiClient( + os.environ["GENERAC_USER"], os.environ["GENERAC_PASS"], session + ) + await api.login() + device_data = await api.get_device_data() + print(json.dumps(device_data, cls=EnhancedJSONEncoder)) + + +# Run it using asyncio.run (preferred method in Python 3.7+) +if __name__ == "__main__": + asyncio.run(main()) From 9d43e1a5922504f86440c77515ff9c8ed1dbaf35 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 01:51:19 -0400 Subject: [PATCH 19/31] Move local test back to root directory since it's not meant to be a unit test --- tests/test_local.py => test_local_access.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/test_local.py => test_local_access.py (100%) diff --git a/tests/test_local.py b/test_local_access.py similarity index 100% rename from tests/test_local.py rename to test_local_access.py From d27d426955f0cdd23afca8ab89c4d8be13d90ddf Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Sun, 20 Jul 2025 02:23:53 -0400 Subject: [PATCH 20/31] Prevent local access test from being used by pytest --- test_local_access.py | 45 -------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 test_local_access.py diff --git a/test_local_access.py b/test_local_access.py deleted file mode 100644 index 9cc715a..0000000 --- a/test_local_access.py +++ /dev/null @@ -1,45 +0,0 @@ -# This script is a standalone test for the Generac API client. -# It logs in to the Generac API using credentials from environment variables -# and prints the device data in JSON format using a custom JSON encoder. -# Make sure to set the environment variables GENERAC_USER and GENERAC_PASS before running this script -# to avoid hardcoding sensitive information in the script. -# This script is intended to be run outside of Home Assistant, for testing purposes. -# It uses the aiohttp library for asynchronous HTTP requests and custom JSON encoding for dataclasses. -# Ensure you have aiohttp installed in your environment. -# If you're using Home Assistant, aiohttp is already included. -# You can install it using pip: pip install aiohttp -import asyncio -import dataclasses -import json -import logging -import os - -import aiohttp # type: ignore -from custom_components.generac.api import GeneracApiClient - -logging.basicConfig(level=logging.DEBUG) - - -# Your custom JSON encoder -class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, o): - if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) - return super().default(o) - - -# Async main logic -async def main(): - jar = aiohttp.CookieJar(unsafe=True, quote_cookie=False) - async with aiohttp.ClientSession(cookie_jar=jar) as session: - api = GeneracApiClient( - os.environ["GENERAC_USER"], os.environ["GENERAC_PASS"], session - ) - await api.login() - device_data = await api.get_device_data() - print(json.dumps(device_data, cls=EnhancedJSONEncoder)) - - -# Run it using asyncio.run (preferred method in Python 3.7+) -if __name__ == "__main__": - asyncio.run(main()) From 17db3a7befd7007ed7374eb3cd296433aff4030d Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 01:43:10 -0400 Subject: [PATCH 21/31] Sort out dependencies --- requirements.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5294c78..58cd631 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ aiohttp==3.12.14 beautifulsoup4==4.13.4 dacite==1.9.2 -homeassistant==2025.7.2 +homeassistant==2025.7.3 voluptuous==0.15.2 pre-commit==4.2.0 reorder-python-imports==3.15.0 -pytest==8.4.1 -pytest-asyncio==1.1.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 From ac2b6f0a2dda4bc1bb49057f16f9dde10ab497cf Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 01:58:55 -0400 Subject: [PATCH 22/31] Remove aiohttp, was readded in rebase by mistake --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 58cd631..ce6a2eb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -aiohttp==3.12.14 beautifulsoup4==4.13.4 dacite==1.9.2 homeassistant==2025.7.3 From 40b47bd421323ab69519ff50bddfaef69b853a67 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:04:00 -0400 Subject: [PATCH 23/31] Fix lint errors --- custom_components/generac/__init__.py | 1 - custom_components/generac/utils.py | 1 - tests/test_weather.py | 1 - 3 files changed, 3 deletions(-) diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index 8390078..293eb05 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -4,7 +4,6 @@ For more details about this integration, please refer to https://github.com/binarydev/generac """ - import logging from homeassistant.config_entries import ConfigEntry diff --git a/custom_components/generac/utils.py b/custom_components/generac/utils.py index 7eef4a2..4b48267 100644 --- a/custom_components/generac/utils.py +++ b/custom_components/generac/utils.py @@ -1,5 +1,4 @@ """Helper to create an aiohttp client session for the Generac API.""" - from aiohttp import ClientSession from aiohttp import CookieJar from homeassistant.core import HomeAssistant diff --git a/tests/test_weather.py b/tests/test_weather.py index 02b292a..4106a67 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -1,5 +1,4 @@ """Test the Generac weather platform.""" - from unittest.mock import MagicMock from custom_components.generac.models import Item, Apparatus, ApparatusDetail, Weather From 3ec5ee3e9d016ff0542b0926efe5fa9bc134771c Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:14:43 -0400 Subject: [PATCH 24/31] Remove flake8 and sort out imports --- .github/workflows/constraints.txt | 1 - .github/workflows/tests.yaml | 2 +- .pre-commit-config.yaml | 6 ---- .vscode/settings.json | 1 - custom_components/generac/__init__.py | 8 +----- custom_components/generac/utils.py | 3 +- tests/test_api.py | 1 + tests/test_binary_sensor.py | 12 ++++---- tests/test_coordinator.py | 3 +- tests/test_diagnostics.py | 2 +- tests/test_entity.py | 2 +- tests/test_image.py | 4 +-- tests/test_sensor.py | 40 ++++++++++++++------------- tests/test_weather.py | 2 +- 14 files changed, 38 insertions(+), 49 deletions(-) diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt index f76801b..83c448e 100644 --- a/.github/workflows/constraints.txt +++ b/.github/workflows/constraints.txt @@ -1,7 +1,6 @@ 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 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c7c7623..55c493c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -33,7 +33,7 @@ jobs: - name: Install Python modules run: | - pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports pipreqs + pip install --constraint=.github/workflows/constraints.txt pre-commit black reorder-python-imports pipreqs - name: Run pre-commit on all files run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b1e0fb..e5a4d12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,12 +8,6 @@ repos: - id: trailing-whitespace - repo: local hooks: - - id: flake8 - name: flake8 - entry: flake8 - language: system - types: [python] - require_serial: true - id: reorder-python-imports name: Reorder python imports entry: reorder-python-imports diff --git a/.vscode/settings.json b/.vscode/settings.json index 112cf93..4307a46 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,6 @@ "editor.defaultFormatter": "ms-python.black-formatter" }, "python.formatting.provider": "none", - "python.linting.flake8Enabled": true, "[json]": { "editor.quickSuggestions": { "strings": true diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index 293eb05..b38b75c 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -11,16 +11,10 @@ from homeassistant.exceptions import ConfigEntryNotReady from .api import GeneracApiClient -from .utils import async_client_session -from .const import CONF_PASSWORD -from .const import CONF_USERNAME -from .const import DOMAIN -from .const import PLATFORMS -from .const import STARTUP_MESSAGE +from .const import CONF_PASSWORD, CONF_USERNAME, DOMAIN, PLATFORMS, STARTUP_MESSAGE from .coordinator import GeneracDataUpdateCoordinator from .utils import async_client_session - _LOGGER: logging.Logger = logging.getLogger(__package__) diff --git a/custom_components/generac/utils.py b/custom_components/generac/utils.py index 4b48267..5d72250 100644 --- a/custom_components/generac/utils.py +++ b/custom_components/generac/utils.py @@ -1,6 +1,5 @@ """Helper to create an aiohttp client session for the Generac API.""" -from aiohttp import ClientSession -from aiohttp import CookieJar +from aiohttp import ClientSession, CookieJar from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client diff --git a/tests/test_api.py b/tests/test_api.py index 7e77084..30be610 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,6 @@ """Test the Generac API.""" import re + import aiohttp import pytest from aioresponses import aioresponses diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 9abcc1b..84a8c71 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -1,11 +1,13 @@ """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 Item, Apparatus, ApparatusDetail +from custom_components.generac.binary_sensor import ( + GeneracConnectedSensor, + GeneracConnectingSensor, + GeneracMaintenanceAlertSensor, + GeneracWarningSensor, +) +from custom_components.generac.models import Apparatus, ApparatusDetail, Item def get_mock_item( diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 2d1721d..e686aca 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -1,6 +1,5 @@ """Test the Generac data update coordinator.""" -from unittest.mock import AsyncMock -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import pytest from homeassistant.helpers.update_coordinator import UpdateFailed diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index d4cbe4d..748b1e8 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock from custom_components.generac.diagnostics import async_get_config_entry_diagnostics -from custom_components.generac.models import Item, Apparatus, ApparatusDetail +from custom_components.generac.models import Apparatus, ApparatusDetail, Item async def test_diagnostics(hass): diff --git a/tests/test_entity.py b/tests/test_entity.py index 69ce34c..fedc545 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock from custom_components.generac.entity import GeneracEntity -from custom_components.generac.models import Item, Apparatus, ApparatusDetail +from custom_components.generac.models import Apparatus, ApparatusDetail, Item def get_mock_item() -> Item: diff --git a/tests/test_image.py b/tests/test_image.py index 1591014..c4d9c47 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -1,10 +1,10 @@ """Test the Generac image platform.""" -from unittest.mock import MagicMock, AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import httpx from custom_components.generac.image import HeroImageSensor -from custom_components.generac.models import Item, Apparatus, ApparatusDetail +from custom_components.generac.models import Apparatus, ApparatusDetail, Item def get_mock_item(hero_image_url: str) -> Item: diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 0f4b6b5..7483969 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,36 +1,38 @@ """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 Item, Apparatus, ApparatusDetail, Weather +from custom_components.generac.const import ( + DEVICE_TYPE_GENERATOR, + DEVICE_TYPE_PROPANE_MONITOR, +) +from custom_components.generac.models import Apparatus, ApparatusDetail, Item, Weather from custom_components.generac.sensor import ( - StatusSensor, - RunTimeSensor, - ProtectionTimeSensor, ActivationDateSensor, - LastSeenSensor, - ConnectionTimeSensor, + AddressSensor, + BatteryLevelSensor, BatteryVoltageSensor, - DeviceTypeSensor, + CapacitySensor, + ConnectionTimeSensor, DealerEmailSensor, DealerNameSensor, DealerPhoneSensor, - AddressSensor, - StatusTextSensor, - StatusLabelSensor, - SerialNumberSensor, - ModelNumberSensor, DeviceSsidSensor, - PanelIDSensor, - SignalStrengthSensor, - CapacitySensor, + DeviceTypeSensor, FuelLevelSensor, FuelTypeSensor, - OrientationSensor, LastReadingDateSensor, - BatteryLevelSensor, + LastSeenSensor, + ModelNumberSensor, + OrientationSensor, OutdoorTemperatureSensor, + PanelIDSensor, + ProtectionTimeSensor, + RunTimeSensor, + SerialNumberSensor, + SignalStrengthSensor, + StatusLabelSensor, + StatusSensor, + StatusTextSensor, ) diff --git a/tests/test_weather.py b/tests/test_weather.py index 4106a67..4ede359 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -1,7 +1,7 @@ """Test the Generac weather platform.""" from unittest.mock import MagicMock -from custom_components.generac.models import Item, Apparatus, ApparatusDetail, Weather +from custom_components.generac.models import Apparatus, ApparatusDetail, Item, Weather from custom_components.generac.weather import WeatherSensor From 3e2180268ae2a75b555d5af352ecf3d991eda7b1 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:16:33 -0400 Subject: [PATCH 25/31] Sort out pre-commits --- custom_components/generac/__init__.py | 6 ++- custom_components/generac/utils.py | 3 +- pytest.ini | 2 +- tests/conftest.py | 1 + tests/test_api.py | 33 ++++++++----- tests/test_binary_sensor.py | 21 ++++---- tests/test_config_flow.py | 6 +-- tests/test_coordinator.py | 8 +-- tests/test_diagnostics.py | 4 +- tests/test_entity.py | 6 ++- tests/test_image.py | 9 ++-- tests/test_sensor.py | 71 ++++++++++++++------------- tests/test_weather.py | 5 +- 13 files changed, 101 insertions(+), 74 deletions(-) diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index b38b75c..1cc0317 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -11,7 +11,11 @@ from homeassistant.exceptions import ConfigEntryNotReady from .api import GeneracApiClient -from .const import CONF_PASSWORD, CONF_USERNAME, DOMAIN, PLATFORMS, STARTUP_MESSAGE +from .const import CONF_PASSWORD +from .const import CONF_USERNAME +from .const import DOMAIN +from .const import PLATFORMS +from .const import STARTUP_MESSAGE from .coordinator import GeneracDataUpdateCoordinator from .utils import async_client_session diff --git a/custom_components/generac/utils.py b/custom_components/generac/utils.py index 5d72250..4b48267 100644 --- a/custom_components/generac/utils.py +++ b/custom_components/generac/utils.py @@ -1,5 +1,6 @@ """Helper to create an aiohttp client session for the Generac API.""" -from aiohttp import ClientSession, CookieJar +from aiohttp import ClientSession +from aiohttp import CookieJar from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client diff --git a/pytest.ini b/pytest.ini index b4546f2..22d38ce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] asyncio_mode = auto markers = - socket: Enables socket connections \ No newline at end of file + socket: Enables socket connections diff --git a/tests/conftest.py b/tests/conftest.py index 5ddbe04..806c0c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ 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.""" diff --git a/tests/test_api.py b/tests/test_api.py index 30be610..82c146d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,8 +4,8 @@ import aiohttp import pytest from aioresponses import aioresponses - -from custom_components.generac.api import GeneracApiClient, get_setting_json +from custom_components.generac.api import GeneracApiClient +from custom_components.generac.api import get_setting_json def test_get_setting_json(): @@ -37,24 +37,30 @@ def test_get_setting_json_no_settings(): 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", + 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.*"), + re.compile( + r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/SelfAsserted.*" + ), status=200, - payload={"status": "200"} + payload={"status": "200"}, ) m.get( - re.compile(r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/api/CombinedSigninAndSignup/confirmed.*"), + re.compile( + r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/api/CombinedSigninAndSignup/confirmed.*" + ), status=200, - body='''
''', + body="""
""", ) m.post("https://app.mobilelinkgen.com/test-action", status=200) - m.get("https://app.mobilelinkgen.com/api/v2/Apparatus/list", + m.get( + "https://app.mobilelinkgen.com/api/v2/Apparatus/list", status=200, payload=[ { @@ -64,9 +70,10 @@ async def test_api_flow(): } ], ) - m.get("https://app.mobilelinkgen.com/api/v1/Apparatus/details/12345", + m.get( + "https://app.mobilelinkgen.com/api/v1/Apparatus/details/12345", status=200, - payload={"key": "value"} + payload={"key": "value"}, ) async with aiohttp.ClientSession() as session: @@ -76,4 +83,4 @@ async def test_api_flow(): 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" \ No newline at end of file + assert client.csrf == "test-csrf" diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 84a8c71..f4dcd5c 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -1,17 +1,20 @@ """Test the Generac binary sensor platform.""" from unittest.mock import MagicMock -from custom_components.generac.binary_sensor import ( - GeneracConnectedSensor, - GeneracConnectingSensor, - GeneracMaintenanceAlertSensor, - GeneracWarningSensor, -) -from custom_components.generac.models import Apparatus, ApparatusDetail, Item +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 + is_connected: bool, + is_connecting: bool, + has_maintenance_alert: bool, + show_warning: bool, ) -> Item: """Return a mock Item object.""" return Item( @@ -94,4 +97,4 @@ async def test_warning_sensor(hass): # 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 \ No newline at end of file + assert sensor.is_on is False diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 9a86d74..3d20bd7 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Generac config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup -from homeassistant.core import HomeAssistant - 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: diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index e686aca..5ea2c59 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -1,10 +1,10 @@ """Test the Generac data update coordinator.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock +from unittest.mock import MagicMock import pytest -from homeassistant.helpers.update_coordinator import UpdateFailed - from custom_components.generac.coordinator import GeneracDataUpdateCoordinator +from homeassistant.helpers.update_coordinator import UpdateFailed async def test_coordinator_init(hass): @@ -40,4 +40,4 @@ async def test_coordinator_update_data_fails(hass): coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry) with pytest.raises(UpdateFailed): await coordinator._async_update_data() - assert not coordinator.is_online \ No newline at end of file + assert not coordinator.is_online diff --git a/tests/test_diagnostics.py b/tests/test_diagnostics.py index 748b1e8..9893e44 100644 --- a/tests/test_diagnostics.py +++ b/tests/test_diagnostics.py @@ -2,7 +2,9 @@ from unittest.mock import MagicMock from custom_components.generac.diagnostics import async_get_config_entry_diagnostics -from custom_components.generac.models import Apparatus, ApparatusDetail, Item +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): diff --git a/tests/test_entity.py b/tests/test_entity.py index fedc545..59a9e88 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -2,7 +2,9 @@ from unittest.mock import MagicMock from custom_components.generac.entity import GeneracEntity -from custom_components.generac.models import Apparatus, ApparatusDetail, Item +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: @@ -50,4 +52,4 @@ async def test_entity(hass): coordinator.data = {"12345": new_item} entity._handle_coordinator_update() assert entity.item == new_item - assert entity.async_write_ha_state.called \ No newline at end of file + assert entity.async_write_ha_state.called diff --git a/tests/test_image.py b/tests/test_image.py index c4d9c47..c3a712b 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -1,10 +1,13 @@ """Test the Generac image platform.""" -from unittest.mock import AsyncMock, MagicMock, patch +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, ApparatusDetail, Item +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: diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 7483969..8fa775e 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,39 +1,38 @@ """Test the Generac sensor platform.""" from unittest.mock import MagicMock -from custom_components.generac.const import ( - DEVICE_TYPE_GENERATOR, - DEVICE_TYPE_PROPANE_MONITOR, -) -from custom_components.generac.models import Apparatus, ApparatusDetail, Item, Weather -from custom_components.generac.sensor import ( - ActivationDateSensor, - AddressSensor, - BatteryLevelSensor, - BatteryVoltageSensor, - CapacitySensor, - ConnectionTimeSensor, - DealerEmailSensor, - DealerNameSensor, - DealerPhoneSensor, - DeviceSsidSensor, - DeviceTypeSensor, - FuelLevelSensor, - FuelTypeSensor, - LastReadingDateSensor, - LastSeenSensor, - ModelNumberSensor, - OrientationSensor, - OutdoorTemperatureSensor, - PanelIDSensor, - ProtectionTimeSensor, - RunTimeSensor, - SerialNumberSensor, - SignalStrengthSensor, - StatusLabelSensor, - StatusSensor, - StatusTextSensor, -) +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( @@ -165,7 +164,9 @@ async def test_generator_sensors(hass): outdoor_temperature=72.0, outdoor_temperature_unit="F", ) - item.apparatus.properties = [MagicMock(type=3, value=MagicMock(signalStrength="100%"))] + item.apparatus.properties = [ + MagicMock(type=3, value=MagicMock(signalStrength="100%")) + ] sensor = RunTimeSensor(coordinator, entry, "12345", item) assert sensor.native_value == 123 @@ -265,4 +266,4 @@ async def test_propane_monitor_sensors(hass): assert sensor.native_value == "456 Oak Ave" sensor = DeviceTypeSensor(coordinator, entry, "12345", item) - assert sensor.native_value == "lte-tankutility-v2" \ No newline at end of file + assert sensor.native_value == "lte-tankutility-v2" diff --git a/tests/test_weather.py b/tests/test_weather.py index 4ede359..a9e66de 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -1,7 +1,10 @@ """Test the Generac weather platform.""" from unittest.mock import MagicMock -from custom_components.generac.models import Apparatus, ApparatusDetail, Item, Weather +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 From ed809c1775596ba68200acc28cb06aff1cb5eafd Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:18:22 -0400 Subject: [PATCH 26/31] Restore flake8 support --- .github/workflows/constraints.txt | 1 + .github/workflows/tests.yaml | 2 +- .pre-commit-config.yaml | 6 ++++++ .vscode/settings.json | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt index 83c448e..f76801b 100644 --- a/.github/workflows/constraints.txt +++ b/.github/workflows/constraints.txt @@ -1,6 +1,7 @@ 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 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 55c493c..c7c7623 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -33,7 +33,7 @@ jobs: - name: Install Python modules run: | - pip install --constraint=.github/workflows/constraints.txt pre-commit black reorder-python-imports pipreqs + pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports pipreqs - name: Run pre-commit on all files run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5a4d12..7b1e0fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,12 @@ repos: - id: trailing-whitespace - repo: local hooks: + - id: flake8 + name: flake8 + entry: flake8 + language: system + types: [python] + require_serial: true - id: reorder-python-imports name: Reorder python imports entry: reorder-python-imports diff --git a/.vscode/settings.json b/.vscode/settings.json index 4307a46..112cf93 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "editor.defaultFormatter": "ms-python.black-formatter" }, "python.formatting.provider": "none", + "python.linting.flake8Enabled": true, "[json]": { "editor.quickSuggestions": { "strings": true From e8a27be6fece1d92cb9551c16f1afcd60ee803ed Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:22:15 -0400 Subject: [PATCH 27/31] Add pytests to the github workflow --- .github/workflows/tests.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c7c7623..4e20917 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -39,6 +39,8 @@ jobs: run: | pre-commit run --all-files --show-diff-on-failure --color=always + + hacs: runs-on: "ubuntu-latest" name: HACS @@ -61,3 +63,29 @@ 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 --constraint=.github/workflows/constraints.txt pip + pip --version + + - name: Install dependencies + run: | + if [ -f requirements.txt ]; then pip install --constraint=.github/workflows/constraints.txt -r requirements.txt; fi + pip install --constraint=.github/workflows/constraints.txt pytest + + - name: Run pytest + run: | + pytest --maxfail=3 --disable-warnings --color=yes From a488e1c09783cc2a03a2069985ceab336e699790 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:23:39 -0400 Subject: [PATCH 28/31] Remove empty lines --- .github/workflows/tests.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4e20917..f57d656 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -39,8 +39,6 @@ jobs: run: | pre-commit run --all-files --show-diff-on-failure --color=always - - hacs: runs-on: "ubuntu-latest" name: HACS From 3094b9a8216ae2f22c16e889eb90e6279f7eb7f5 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:25:21 -0400 Subject: [PATCH 29/31] Remove constraints for unit tests --- .github/workflows/tests.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f57d656..1f5b720 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -76,13 +76,12 @@ jobs: - name: Upgrade pip run: | - pip install --constraint=.github/workflows/constraints.txt pip + pip install pip pip --version - name: Install dependencies run: | - if [ -f requirements.txt ]; then pip install --constraint=.github/workflows/constraints.txt -r requirements.txt; fi - pip install --constraint=.github/workflows/constraints.txt pytest + pip install -r requirements.txt - name: Run pytest run: | From 6d514af07e042164df6c2015718ba35024fbc521 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:31:11 -0400 Subject: [PATCH 30/31] Bump python default version to 3.13 --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1f5b720..3ecd9ac 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: From 4bc40166986c31e518821da8000580896bced2b4 Mon Sep 17 00:00:00 2001 From: Jose Santiago Date: Fri, 25 Jul 2025 02:33:03 -0400 Subject: [PATCH 31/31] Remove package constraints for github workflows --- .github/workflows/constraints.txt | 8 -------- .github/workflows/tests.yaml | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 .github/workflows/constraints.txt 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 3ecd9ac..ff85b5c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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: |