From db5a47161c33ce3eb7b776d3dc529e03b504b5ee Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 10:46:48 -0500 Subject: [PATCH 01/14] fix casing --- .bruno/environments/default.bru | 3 ++ custom_components/ac_infinity/__init__.py | 12 +++---- custom_components/ac_infinity/config_flow.py | 12 +++---- custom_components/ac_infinity/const.py | 10 +++--- custom_components/ac_infinity/core.py | 6 ++-- tests/data_models.py | 24 ++++++------- tests/test_init.py | 36 ++++++++++---------- 7 files changed, 53 insertions(+), 50 deletions(-) diff --git a/.bruno/environments/default.bru b/.bruno/environments/default.bru index be1e3af..70f2e77 100644 --- a/.bruno/environments/default.bru +++ b/.bruno/environments/default.bru @@ -1,3 +1,6 @@ +vars { + USER_AGENT: okhttp/4.12.0 +} vars:secret [ EMAIL, DEVICE_ID, diff --git a/custom_components/ac_infinity/__init__.py b/custom_components/ac_infinity/__init__.py index 4b8d4c9..0871496 100644 --- a/custom_components/ac_infinity/__init__.py +++ b/custom_components/ac_infinity/__init__.py @@ -75,12 +75,12 @@ async def __initialize_new_devices_if_any( port_count = ac_infinity.get_controller_property(device_id, ControllerPropertyKey.PORT_COUNT) device_config = { - "controller": EntityConfigValue.SensorsOnly, - "sensors": EntityConfigValue.SensorsOnly, + "controller": EntityConfigValue.SENSORS_ONLY, + "sensors": EntityConfigValue.SENSORS_ONLY, } for i in range(1, port_count + 1): - device_config[f"port_{i}"] = EntityConfigValue.SensorsOnly + device_config[f"port_{i}"] = EntityConfigValue.SENSORS_ONLY entities_config[str(device_id)] = device_config @@ -132,12 +132,12 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> device_name = ac_infinity.get_controller_property(device_id, ControllerPropertyKey.DEVICE_NAME, f"Device {device_id}") device_config = { - "controller": EntityConfigValue.SensorsAndSettings, - "sensors": EntityConfigValue.SensorsOnly, + "controller": EntityConfigValue.SENSORS_AND_SETTINGS, + "sensors": EntityConfigValue.SENSORS_ONLY, } for i in range(1, port_count + 1): - device_config[f"port_{i}"] = EntityConfigValue.All + device_config[f"port_{i}"] = EntityConfigValue.ALL new_data[ConfigurationKey.ENTITIES][str(device_id)] = device_config diff --git a/custom_components/ac_infinity/config_flow.py b/custom_components/ac_infinity/config_flow.py index 32d4871..495af41 100644 --- a/custom_components/ac_infinity/config_flow.py +++ b/custom_components/ac_infinity/config_flow.py @@ -30,11 +30,11 @@ _LOGGER = logging.getLogger(__name__) # Individual entity configuration option constants -OPTION_ALL_ENTITIES = {"value": EntityConfigValue.All, "label": "Sensors, Controls, and Settings"} -OPTION_SENSORS_AND_SETTINGS = {"value": EntityConfigValue.SensorsAndSettings, "label": "Sensors and Settings"} -OPTION_SENSORS_AND_CONTROLS = {"value": EntityConfigValue.SensorsAndControls, "label": "Sensors and Controls"} -OPTION_SENSORS_ONLY = {"value": EntityConfigValue.SensorsOnly, "label": "Sensors Only"} -OPTION_DISABLE = {"value": EntityConfigValue.Disable, "label": "Disable"} +OPTION_ALL_ENTITIES = {"value": EntityConfigValue.ALL, "label": "Sensors, Controls, and Settings"} +OPTION_SENSORS_AND_SETTINGS = {"value": EntityConfigValue.SENSORS_AND_SETTINGS, "label": "Sensors and Settings"} +OPTION_SENSORS_AND_CONTROLS = {"value": EntityConfigValue.SENSORS_AND_CONTROLS, "label": "Sensors and Controls"} +OPTION_SENSORS_ONLY = {"value": EntityConfigValue.SENSORS_ONLY, "label": "Sensors Only"} +OPTION_DISABLE = {"value": EntityConfigValue.DISABLE, "label": "Disable"} # Entity configuration option arrays ENTITY_CONFIG_OPTIONS_CONTROLLER = [ @@ -68,7 +68,7 @@ def __get_saved_entity_conf_value(data: dict[str, Any] | None, device_id: str, e """Get saved entity configuration value from config entry data.""" if data is None: # ConfigFlow - we are setting the integration up for the first time. Don't add excessive entities. - return EntityConfigValue.SensorsOnly + return EntityConfigValue.SENSORS_ONLY return ( data[ConfigurationKey.ENTITIES][device_id][entity_config_key] diff --git a/custom_components/ac_infinity/const.py b/custom_components/ac_infinity/const.py index fe65afd..4d30aa2 100644 --- a/custom_components/ac_infinity/const.py +++ b/custom_components/ac_infinity/const.py @@ -26,11 +26,11 @@ class ConfigurationKey: class EntityConfigValue: - All = "all" - SensorsAndSettings = "sensors_and_settings" - SensorsAndControls = "sensors_and_controls" - SensorsOnly = "sensors_only" - Disable = "disable" + ALL = "all" + SENSORS_AND_SETTINGS = "sensors_and_settings" + SENSORS_AND_CONTROLS = "sensors_and_controls" + SENSORS_ONLY = "sensors_only" + DISABLE = "disable" class CustomDevicePropertyKey: diff --git a/custom_components/ac_infinity/core.py b/custom_components/ac_infinity/core.py index d7fd256..72bc8ed 100644 --- a/custom_components/ac_infinity/core.py +++ b/custom_components/ac_infinity/core.py @@ -1151,15 +1151,15 @@ def append_if_suitable(self, entity: ACInfinityEntity): def enabled_fn_sensor(entry: ConfigEntry, device_id: str, entity_config_key: str) -> bool: - return entry.data[ConfigurationKey.ENTITIES][device_id][entity_config_key] != EntityConfigValue.Disable + return entry.data[ConfigurationKey.ENTITIES][device_id][entity_config_key] != EntityConfigValue.DISABLE def enabled_fn_control(entry: ConfigEntry, device_id: str, entity_config_key: str) -> bool: setting = entry.data[ConfigurationKey.ENTITIES][device_id][entity_config_key] - return setting == EntityConfigValue.All or setting == EntityConfigValue.SensorsAndControls + return setting == EntityConfigValue.ALL or setting == EntityConfigValue.SENSORS_AND_CONTROLS def enabled_fn_setting(entry: ConfigEntry, device_id: str, entity_config_key: str) -> bool: setting = entry.data[ConfigurationKey.ENTITIES][device_id][entity_config_key] - return setting == EntityConfigValue.All or setting == EntityConfigValue.SensorsAndSettings + return setting == EntityConfigValue.ALL or setting == EntityConfigValue.SENSORS_AND_SETTINGS diff --git a/tests/data_models.py b/tests/data_models.py index 775f02c..916ef49 100644 --- a/tests/data_models.py +++ b/tests/data_models.py @@ -29,20 +29,20 @@ ConfigurationKey.POLLING_INTERVAL: 10, ConfigurationKey.ENTITIES: { str(DEVICE_ID): { - "controller": EntityConfigValue.All, - "sensors": EntityConfigValue.All, - "port_1": EntityConfigValue.All, - "port_2": EntityConfigValue.All, - "port_3": EntityConfigValue.All, - "port_4": EntityConfigValue.All, + "controller": EntityConfigValue.ALL, + "sensors": EntityConfigValue.ALL, + "port_1": EntityConfigValue.ALL, + "port_2": EntityConfigValue.ALL, + "port_3": EntityConfigValue.ALL, + "port_4": EntityConfigValue.ALL, }, str(AI_DEVICE_ID): { - "controller": EntityConfigValue.All, - "sensors": EntityConfigValue.All, - "port_1": EntityConfigValue.All, - "port_2": EntityConfigValue.All, - "port_3": EntityConfigValue.All, - "port_4": EntityConfigValue.All, + "controller": EntityConfigValue.ALL, + "sensors": EntityConfigValue.ALL, + "port_1": EntityConfigValue.ALL, + "port_2": EntityConfigValue.ALL, + "port_3": EntityConfigValue.ALL, + "port_4": EntityConfigValue.ALL, }, } } diff --git a/tests/test_init.py b/tests/test_init.py index 8dbd074..07e6693 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -213,12 +213,12 @@ async def test_async_migrate_entry_version_1_to_2_success(self, mocker: MockFixt # Verify device configuration structure for device_id in [DEVICE_ID, AI_DEVICE_ID]: device_config = entities_config[str(device_id)] - assert device_config["controller"] == EntityConfigValue.SensorsAndSettings - assert device_config["sensors"] == EntityConfigValue.SensorsOnly - assert device_config["port_1"] == EntityConfigValue.All - assert device_config["port_2"] == EntityConfigValue.All - assert device_config["port_3"] == EntityConfigValue.All - assert device_config["port_4"] == EntityConfigValue.All + assert device_config["controller"] == EntityConfigValue.SENSORS_AND_SETTINGS + assert device_config["sensors"] == EntityConfigValue.SENSORS_ONLY + assert device_config["port_1"] == EntityConfigValue.ALL + assert device_config["port_2"] == EntityConfigValue.ALL + assert device_config["port_3"] == EntityConfigValue.ALL + assert device_config["port_4"] == EntityConfigValue.ALL # Verify service methods were called mock_ac_infinity.refresh.assert_called_once() @@ -399,19 +399,19 @@ async def test_initialize_new_devices_multiple_new_devices(self, mocker: MockFix # Verify first device (2 ports) device1_config = new_data[ConfigurationKey.ENTITIES][str(new_device_id_1)] - assert device1_config["controller"] == EntityConfigValue.SensorsOnly - assert device1_config["sensors"] == EntityConfigValue.SensorsOnly - assert device1_config["port_1"] == EntityConfigValue.SensorsOnly - assert device1_config["port_2"] == EntityConfigValue.SensorsOnly + assert device1_config["controller"] == EntityConfigValue.SENSORS_ONLY + assert device1_config["sensors"] == EntityConfigValue.SENSORS_ONLY + assert device1_config["port_1"] == EntityConfigValue.SENSORS_ONLY + assert device1_config["port_2"] == EntityConfigValue.SENSORS_ONLY assert "port_3" not in device1_config # Should not have port_3 # Verify second device (6 ports) device2_config = new_data[ConfigurationKey.ENTITIES][str(new_device_id_2)] - assert device2_config["controller"] == EntityConfigValue.SensorsOnly - assert device2_config["sensors"] == EntityConfigValue.SensorsOnly - assert device2_config["port_1"] == EntityConfigValue.SensorsOnly - assert device2_config["port_2"] == EntityConfigValue.SensorsOnly - assert device2_config["port_3"] == EntityConfigValue.SensorsOnly - assert device2_config["port_4"] == EntityConfigValue.SensorsOnly - assert device2_config["port_5"] == EntityConfigValue.SensorsOnly - assert device2_config["port_6"] == EntityConfigValue.SensorsOnly + assert device2_config["controller"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["sensors"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["port_1"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["port_2"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["port_3"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["port_4"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["port_5"] == EntityConfigValue.SENSORS_ONLY + assert device2_config["port_6"] == EntityConfigValue.SENSORS_ONLY From aa6ca92bcec21433be4b7ecad49f9d0f21867876 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 12:04:54 -0500 Subject: [PATCH 02/14] pylance --- .github/workflows/tests.yaml | 2 + .vscode/settings.json | 7 ++ .../ac_infinity/binary_sensor.py | 18 ++--- custom_components/ac_infinity/client.py | 20 +++--- custom_components/ac_infinity/const.py | 14 ++++ custom_components/ac_infinity/core.py | 14 ++-- custom_components/ac_infinity/number.py | 66 ++++++++++--------- custom_components/ac_infinity/select.py | 4 +- custom_components/ac_infinity/sensor.py | 23 +++---- custom_components/ac_infinity/switch.py | 6 +- custom_components/ac_infinity/time.py | 4 +- pyproject.toml | 10 ++- requirements.txt | 2 +- tests/__init__.py | 13 ++-- tests/test_init.py | 7 +- 15 files changed, 122 insertions(+), 88 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 66a6eb6..670bc0d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -19,6 +19,8 @@ jobs: run: pip install --upgrade -r requirements.txt - name: Run Unit Tests run: pytest --cov + - name: Run Type Checks + run: pyright - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/custom_components/ac_infinity/binary_sensor.py b/custom_components/ac_infinity/binary_sensor.py index a673466..b82b5be 100644 --- a/custom_components/ac_infinity/binary_sensor.py +++ b/custom_components/ac_infinity/binary_sensor.py @@ -1,5 +1,6 @@ import logging from dataclasses import dataclass +from typing import Any from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -14,6 +15,7 @@ DOMAIN, ControllerPropertyKey, DevicePropertyKey, + MdiIcon, SensorPropertyKey, SensorReferenceKey, SensorType, @@ -42,9 +44,9 @@ class ACInfinityBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes ACInfinity Binary Sensor Entities.""" key: str - device_class: BinarySensorDeviceClass | None - icon: str | None - translation_key: str | None + device_class: BinarySensorDeviceClass | None = None + icon: str | None = None + translation_key: str | None = None @dataclass(frozen=True) @@ -92,7 +94,7 @@ def __get_value_fn_controller_property_default( def __get_value_fn_device_property_default( entity: ACInfinityEntity, device: ACInfinityDevice -): +) -> Any: return entity.ac_infinity.get_device_property( device.controller.controller_id, device.device_port, entity.data_key, False ) @@ -130,7 +132,7 @@ def __get_value_fn_sensor_value_default( ACInfinityControllerBinarySensorEntityDescription( key=ControllerPropertyKey.ONLINE, device_class=BinarySensorDeviceClass.CONNECTIVITY, - icon="mdi:power-plug", + icon=MdiIcon.POWER_PLUG, translation_key="controller_online", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_controller_property_default, @@ -142,7 +144,7 @@ def __get_value_fn_sensor_value_default( SensorType.WATER: ACInfinitySensorBinarySensorEntityDescription( key=SensorReferenceKey.WATER, device_class=BinarySensorDeviceClass.MOISTURE, - icon="mdi:waves", + icon=MdiIcon.WAVES, translation_key="water_sensor", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_sensor_default, @@ -154,7 +156,7 @@ def __get_value_fn_sensor_value_default( ACInfinityDeviceBinarySensorEntityDescription( key=DevicePropertyKey.ONLINE, device_class=BinarySensorDeviceClass.CONNECTIVITY, - icon="mdi:power-plug", + icon=MdiIcon.POWER_PLUG, translation_key="port_online", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_device_property_default, @@ -163,7 +165,7 @@ def __get_value_fn_sensor_value_default( ACInfinityDeviceBinarySensorEntityDescription( key=DevicePropertyKey.STATE, device_class=BinarySensorDeviceClass.POWER, - icon="mdi:power", + icon=MdiIcon.POWER, translation_key="port_state", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_device_property_default, diff --git a/custom_components/ac_infinity/client.py b/custom_components/ac_infinity/client.py index 250d40a..41d3248 100644 --- a/custom_components/ac_infinity/client.py +++ b/custom_components/ac_infinity/client.py @@ -54,12 +54,16 @@ def is_logged_in(self): """returns true if the user id is set, false otherwise""" return True if self._user_id else False + def __ensure_logged_in(self) -> None: + """Raise when a request requires an authenticated client.""" + if not self.is_logged_in(): + raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + async def get_account_controllers(self): """Obtains a list of controllers, including metadata and some sensor values. Does not include information related to settings. """ - if not self.is_logged_in(): - raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + self.__ensure_logged_in() headers = self.__create_headers(use_auth_token=True) body = await self.__post( @@ -75,8 +79,7 @@ async def get_device_mode_settings(self, controller_id: str | int, device_port: controller_id: The parent controller id of the port device_port: The port on the controller of the settings list to grab """ - if not self.is_logged_in(): - raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + self.__ensure_logged_in() headers = self.__create_headers(use_auth_token=True) body = await self.__post( @@ -110,8 +113,7 @@ async def update_device_controls( device_port: The port on the controller the device is plugged into key_values: The key value pairs of settings to set """ - if not self.is_logged_in(): - raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + self.__ensure_logged_in() headers = self.__create_headers(use_auth_token=True) body = await self.__post( @@ -139,8 +141,7 @@ async def update_device_settings( device_name: The name of the device key_values: The key value pairs of settings to set """ - if not self.is_logged_in(): - raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + self.__ensure_logged_in() headers = self.__create_headers(use_auth_token=True) body = await self.__post( @@ -169,8 +170,7 @@ async def update_ai_device_control_and_settings( device_port: port of the device key_values: The key value pairs of settings to set """ - if not self.is_logged_in(): - raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.") + self.__ensure_logged_in() headers = self.__create_headers(use_auth_token=True, use_min_version=True) body = await self.__post( diff --git a/custom_components/ac_infinity/const.py b/custom_components/ac_infinity/const.py index 4d30aa2..8be0480 100644 --- a/custom_components/ac_infinity/const.py +++ b/custom_components/ac_infinity/const.py @@ -17,6 +17,20 @@ DEFAULT_POLLING_INTERVAL = 10 ISSUE_URL = "https://github.com/dalinicus/homeassistant-acinfinity/issues/new?template=Blank+issue" +class MdiIcon: + THERMOMETER_PLUS = "mdi:thermometer-plus" + CLOUD_PERCENT_OUTLINE = "mdi:cloud-percent-outline" + LEAF = "mdi:leaf" + KNOB = "mdi:knob" + WATER_THERMOMETER_OUTLINE = "mdi:water-thermometer-outline" + WATER_PERCENT = "mdi:water-percent" + WATER_THERMOMETER = "mdi:water-thermometer" + LIGHTBULB_ON_OUTLINE = "mdi:lightbulb-on-outline" + WATERING_CAN_OUTLINE = "mdi:watering-can-outline" + POWER_PLUG = "mdi:power-plug" + WAVES = "mdi:waves" + POWER = "mdi:power" + class ConfigurationKey: POLLING_INTERVAL = "polling_interval" diff --git a/custom_components/ac_infinity/core.py b/custom_components/ac_infinity/core.py index 72bc8ed..5464ebb 100644 --- a/custom_components/ac_infinity/core.py +++ b/custom_components/ac_infinity/core.py @@ -10,7 +10,7 @@ import aiohttp import async_timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -353,7 +353,7 @@ def get_controller_property_exists( def get_controller_property( self, controller_id: str | int, property_key: str, default_value=None - ): + ) -> Any: """gets a property value for a given controller, if both the property and controller exist. Args: @@ -401,7 +401,7 @@ def get_sensor_property( sensor_type: int, property_key: str, default_value=None, - ): + ) -> Any: """gets a property value for a given sensor on a controller, if the property, controller, access port, and sensor all exist. Args: @@ -445,7 +445,7 @@ def get_device_property( device_port: int, property_key: str, default_value=None, - ): + ) -> Any: """gets a property value for a given port on a controller, if the property, controller and port all exist. Args: @@ -476,7 +476,7 @@ def get_controller_setting_exists( def get_controller_setting( self, controller_id: str | int, setting_key: str, default_value=None - ): + ) -> Any: """gets a property value for a given controller, if both the property and controller exist. Args: @@ -505,7 +505,7 @@ def get_device_setting( device_port: int, setting_key: str, default_value=None, - ): + ) -> Any: """gets a property value for a given device, if both the setting and device exist. Args: @@ -551,7 +551,7 @@ def get_device_control( device_port: int, setting_key: str, default_value=None, - ): + ) -> Any: """gets the current set value for a given device setting Args: diff --git a/custom_components/ac_infinity/number.py b/custom_components/ac_infinity/number.py index 92aef33..7e980c4 100644 --- a/custom_components/ac_infinity/number.py +++ b/custom_components/ac_infinity/number.py @@ -13,7 +13,11 @@ from homeassistant.core import HomeAssistant from custom_components.ac_infinity.const import ( - AtType, DOMAIN, AdvancedSettingsKey, DeviceControlKey, + AtType, + DOMAIN, + AdvancedSettingsKey, + DeviceControlKey, + MdiIcon, ) from custom_components.ac_infinity.core import ( ACInfinityController, @@ -35,14 +39,14 @@ class ACInfinityNumberEntityDescription(NumberEntityDescription): """Describes ACInfinity Number Entities""" key: str - icon: str | None - translation_key: str | None - device_class: NumberDeviceClass | None - mode: NumberMode | None - native_max_value: float | None - native_min_value: float | None - native_step: float | None - native_unit_of_measurement: str | None + icon: str | None = None + translation_key: str | None = None + device_class: NumberDeviceClass | None = None + mode: NumberMode | None = None + native_max_value: float | None = None + native_min_value: float | None = None + native_step: float | None = None + native_unit_of_measurement: str | None = None @dataclass(frozen=True) @@ -458,7 +462,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=-20, native_max_value=20, native_step=1, - icon="mdi:thermometer-plus", + icon=MdiIcon.THERMOMETER_PLUS, translation_key="temperature_calibration", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -474,7 +478,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=-10, native_max_value=10, native_step=1, - icon="mdi:thermometer-plus", + icon=MdiIcon.THERMOMETER_PLUS, translation_key="temperature_calibration", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -489,7 +493,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=-10, native_max_value=10, native_step=1, - icon="mdi:cloud-percent-outline", + icon=MdiIcon.CLOUD_PERCENT_OUTLINE, translation_key="humidity_calibration", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -505,7 +509,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=-20, native_max_value=20, native_step=1, - icon="mdi:leaf", + icon=MdiIcon.LEAF, translation_key="vpd_leaf_temperature_offset", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -521,7 +525,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=-10, native_max_value=10, native_step=1, - icon="mdi:leaf", + icon=MdiIcon.LEAF, translation_key="vpd_leaf_temperature_offset", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -539,7 +543,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:knob", + icon=MdiIcon.KNOB, translation_key="on_power", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -555,7 +559,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:knob", + icon=MdiIcon.KNOB, translation_key="off_power", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -571,7 +575,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:knob", + icon=MdiIcon.KNOB, translation_key="on_power", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -651,7 +655,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=9.9, native_step=0.1, - icon="mdi:water-thermometer-outline", + icon=MdiIcon.WATER_THERMOMETER_OUTLINE, translation_key="vpd_mode_low_trigger", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -667,7 +671,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=9.9, native_step=0.1, - icon="mdi:water-thermometer-outline", + icon=MdiIcon.WATER_THERMOMETER_OUTLINE, translation_key="vpd_mode_high_trigger", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -683,7 +687,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=9.9, native_step=0.1, - icon="mdi:water-thermometer-outline", + icon=MdiIcon.WATER_THERMOMETER_OUTLINE, translation_key="target_vpd", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -699,7 +703,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=100, native_step=1, - icon="mdi:water-percent", + icon=MdiIcon.WATER_PERCENT, translation_key="auto_mode_humidity_low_trigger", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -715,7 +719,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=100, native_step=1, - icon="mdi:water-percent", + icon=MdiIcon.WATER_PERCENT, translation_key="auto_mode_humidity_high_trigger", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -731,7 +735,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=100, native_step=1, - icon="mdi:water-percent", + icon=MdiIcon.WATER_PERCENT, translation_key="target_humidity", native_unit_of_measurement=None, enabled_fn=enabled_fn_control, @@ -796,7 +800,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=20, native_step=1, - icon="mdi:thermometer-plus", + icon=MdiIcon.THERMOMETER_PLUS, translation_key="dynamic_transition_temp", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -813,7 +817,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:thermometer-plus", + icon=MdiIcon.THERMOMETER_PLUS, translation_key="dynamic_transition_temp", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -829,7 +833,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:cloud-percent-outline", + icon=MdiIcon.CLOUD_PERCENT_OUTLINE, translation_key="dynamic_transition_humidity", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -845,7 +849,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=1, native_step=0.1, - icon="mdi:leaf", + icon=MdiIcon.LEAF, translation_key="dynamic_transition_vpd", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -862,7 +866,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=20, native_step=1, - icon="mdi:thermometer-plus", + icon=MdiIcon.THERMOMETER_PLUS, translation_key="dynamic_buffer_temp", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -879,7 +883,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:thermometer-plus", + icon=MdiIcon.THERMOMETER_PLUS, translation_key="dynamic_buffer_temp", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -895,7 +899,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=10, native_step=1, - icon="mdi:cloud-percent-outline", + icon=MdiIcon.CLOUD_PERCENT_OUTLINE, translation_key="dynamic_buffer_humidity", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, @@ -911,7 +915,7 @@ def __set_value_fn_dynamic_buffer_temp( native_min_value=0, native_max_value=1, native_step=0.1, - icon="mdi:leaf", + icon=MdiIcon.LEAF, translation_key="dynamic_buffer_vpd", native_unit_of_measurement=None, enabled_fn=enabled_fn_setting, diff --git a/custom_components/ac_infinity/select.py b/custom_components/ac_infinity/select.py index e18a4d4..540c103 100644 --- a/custom_components/ac_infinity/select.py +++ b/custom_components/ac_infinity/select.py @@ -29,8 +29,8 @@ class ACInfinitySelectEntityDescription(SelectEntityDescription): """Describes ACInfinity Select Entities.""" key: str - translation_key: str | None - options: list[str] | None + translation_key: str | None = None + options: list[str] | None = None @dataclass(frozen=True) diff --git a/custom_components/ac_infinity/sensor.py b/custom_components/ac_infinity/sensor.py index 560c960..4c121a5 100644 --- a/custom_components/ac_infinity/sensor.py +++ b/custom_components/ac_infinity/sensor.py @@ -39,6 +39,7 @@ from .const import ( DOMAIN, + MdiIcon, ISSUE_URL, ControllerPropertyKey, CustomDevicePropertyKey, @@ -56,12 +57,12 @@ class ACInfinitySensorEntityDescription(SensorEntityDescription): """Describes ACInfinity Number Sensor Entities.""" key: str - icon: str | None - translation_key: str | None - device_class: SensorDeviceClass | None - native_unit_of_measurement: str | None - state_class: SensorStateClass | str | None - suggested_unit_of_measurement: str | None + icon: str | None = None + translation_key: str | None = None + device_class: SensorDeviceClass | None = None + native_unit_of_measurement: str | None = None + state_class: SensorStateClass | str | None = None + suggested_unit_of_measurement: str | None = None @dataclass(frozen=True) @@ -264,7 +265,7 @@ def __get_next_mode_change_timestamp( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.KPA, suggested_unit_of_measurement=None, - icon="mdi:water-thermometer", + icon=MdiIcon.WATER_THERMOMETER, translation_key="vapor_pressure_deficit", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_controller_property_default, @@ -315,7 +316,7 @@ def __get_next_mode_change_timestamp( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.KPA, suggested_unit_of_measurement=None, - icon="mdi:water-thermometer", + icon=MdiIcon.WATER_THERMOMETER, translation_key="probe_vapor_pressure_deficit", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_sensor_default, @@ -363,7 +364,7 @@ def __get_next_mode_change_timestamp( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.KPA, suggested_unit_of_measurement=None, - icon="mdi:water-thermometer", + icon=MdiIcon.WATER_THERMOMETER, translation_key="controller_vapor_pressure_deficit", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_sensor_default, @@ -387,7 +388,7 @@ def __get_next_mode_change_timestamp( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, suggested_unit_of_measurement=None, - icon="mdi:lightbulb-on-outline", + icon=MdiIcon.LIGHTBULB_ON_OUTLINE, translation_key="light_sensor", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_sensor_default, @@ -399,7 +400,7 @@ def __get_next_mode_change_timestamp( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, suggested_unit_of_measurement=None, - icon="mdi:watering-can-outline", + icon=MdiIcon.WATERING_CAN_OUTLINE, translation_key="soil_sensor", enabled_fn=enabled_fn_sensor, suitable_fn=__suitable_fn_sensor_default, diff --git a/custom_components/ac_infinity/switch.py b/custom_components/ac_infinity/switch.py index ce7f473..7d88d7f 100644 --- a/custom_components/ac_infinity/switch.py +++ b/custom_components/ac_infinity/switch.py @@ -47,9 +47,9 @@ class ACInfinitySwitchEntityDescription(SwitchEntityDescription): """Describes ACInfinity Switch Entities.""" key: str - device_class: SwitchDeviceClass | None - icon: str | None - translation_key: str | None + device_class: SwitchDeviceClass | None = None + icon: str | None = None + translation_key: str | None = None @dataclass(frozen=True) diff --git a/custom_components/ac_infinity/time.py b/custom_components/ac_infinity/time.py index f4bffe8..a2eb890 100644 --- a/custom_components/ac_infinity/time.py +++ b/custom_components/ac_infinity/time.py @@ -49,8 +49,8 @@ class ACInfinityTimeEntityDescription(TimeEntityDescription): """Describes ACInfinity Time Entities.""" key: str - icon: str | None - translation_key: str | None + icon: str | None = None + translation_key: str | None = None @dataclass(frozen=True) diff --git a/pyproject.toml b/pyproject.toml index 788e2ad..5b03aae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,9 +96,7 @@ asyncio_default_fixture_loop_scope = "function" [tool.codespell] skip = "custom_components/ac_infinity/translations/*" -[tool.mypy] -python_version = "3.13" -explicit_package_bases = true -ignore_missing_imports = true -check_untyped_defs = true -files = "custom_components,tests" +[tool.pyright] +pythonVersion = "3.13" +typeCheckingMode = "basic" +include = ["custom_components", "tests"] diff --git a/requirements.txt b/requirements.txt index 2f415f2..28fe64c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,6 @@ black codespell ruff aioresponses -mypy +pyright python-dateutil freezegun diff --git a/tests/__init__.py b/tests/__init__.py index 6225199..31ef5b8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,9 +2,10 @@ import asyncio from asyncio import Future +from copy import deepcopy from types import MappingProxyType from typing import Union -from unittest.mock import AsyncMock, MagicMock, NonCallableMagicMock +from unittest.mock import AsyncMock, MagicMock, NonCallableMagicMock, PropertyMock from homeassistant.config_entries import ConfigEntries, ConfigEntry from homeassistant.const import CONF_EMAIL @@ -161,7 +162,7 @@ def setup_entity_mocks(mocker: MockFixture): config_entry = ConfigEntry( entry_id=ENTRY_ID, - data=CONFIG_ENTRY_DATA, + data=deepcopy(CONFIG_ENTRY_DATA), domain=DOMAIN, minor_version=0, source="", @@ -214,9 +215,13 @@ def setup_entity_mocks(mocker: MockFixture): config_flow.hass = hass options_flow = OptionsFlow() - mocker.patch.object(OptionsFlow, "config_entry") + mocker.patch.object( + OptionsFlow, + "config_entry", + new_callable=PropertyMock, + return_value=config_entry, + ) options_flow.hass = hass - options_flow.config_entry = config_entry return ACTestObjects( hass, diff --git a/tests/test_init.py b/tests/test_init.py index 07e6693..1629d48 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,5 +1,6 @@ import asyncio from asyncio import Future +from copy import deepcopy from types import MappingProxyType from unittest.mock import AsyncMock, MagicMock @@ -56,7 +57,7 @@ def setup(mocker: MockFixture): config_entry = ConfigEntry( entry_id=ENTRY_ID, - data=CONFIG_ENTRY_DATA, + data=deepcopy(CONFIG_ENTRY_DATA), domain=DOMAIN, minor_version=0, source="", @@ -279,7 +280,7 @@ async def test_async_migrate_entry_version_2_no_migration_needed(self, mocker: M # Create a version 2 config entry (current version) v2_config_entry = ConfigEntry( entry_id=ENTRY_ID, - data=CONFIG_ENTRY_DATA, # Already has entity configuration + data=deepcopy(CONFIG_ENTRY_DATA), # Already has entity configuration domain=DOMAIN, minor_version=0, source="", @@ -310,7 +311,7 @@ async def test_async_migrate_entry_version_2_no_migration_needed(self, mocker: M async def test_initialize_new_devices_multiple_new_devices(self, mocker: MockFixture): """Test adding multiple new devices with different port counts during setup""" # Create a config entry with no existing devices - empty_data = CONFIG_ENTRY_DATA.copy() + empty_data = deepcopy(CONFIG_ENTRY_DATA) empty_data[ConfigurationKey.ENTITIES] = {} config_entry = ConfigEntry( From 77633fb5b015520d964d06664692c33bd59a9be8 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 12:16:32 -0500 Subject: [PATCH 03/14] sonar --- .github/workflows/tests.yaml | 23 ++++++++++++++++------- DEVELOPMENT.md | 13 ++++++------- README.md | 4 ++-- sonar-project.properties | 9 +++++++++ 4 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 sonar-project.properties diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 670bc0d..4072e39 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -8,8 +8,10 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 name: Setup Python with: python-version: 3.13 @@ -18,10 +20,17 @@ jobs: - name: install dependencies run: pip install --upgrade -r requirements.txt - name: Run Unit Tests - run: pytest --cov + run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term - name: Run Type Checks run: pyright - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v5.2.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + - name: SonarQube Quality Gate + uses: SonarSource/sonarqube-quality-gate-action@v1.2.0 + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e50c998..81d12df 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -279,8 +279,8 @@ async def test_entity_value(setup): ### Running Tests ```bash -# Run all tests with coverage -pytest --cov +# Run all tests with coverage XML report (used by SonarQube) +pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term # Run specific test file pytest tests/test_sensor.py -v @@ -306,9 +306,8 @@ pytest -v ### CI/CD GitHub Actions runs: -- `pytest --cov` for testing -- `ruff check` for linting -- `mypy` for type checking -- Coverage reporting +- `pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term` for testing and coverage collection +- SonarQube scan for static analysis +- SonarQube quality gate to block PRs when conditions are not met -Access Home Assistant at `http://localhost:8123` \ No newline at end of file +Access Home Assistant at `http://localhost:8123` diff --git a/README.md b/README.md index d15c657..a1030ad 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # homeassistant-acinfinity [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) - -[![codecov](https://codecov.io/gh/dalinicus/homeassistant-acinfinity/graph/badge.svg?token=C4TMDAU344)](https://codecov.io/gh/dalinicus/homeassistant-acinfinity) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dalinicus_homeassistant-acinfinity&metric=coverage)](https://sonarcloud.io/summary/new_code?id=dalinicus_homeassistant-acinfinity) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dalinicus_homeassistant-acinfinity&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=dalinicus_homeassistant-acinfinity) [![Tests](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/tests.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/tests.yaml) [![HACS/HASS](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/validate.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/validate.yaml) diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..de39b38 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.projectKey=dalinicus_homeassistant-acinfinity +sonar.projectName=homeassistant-acinfinity +sonar.sources=custom_components +sonar.tests=tests +sonar.python.version=3.13 +sonar.python.coverage.reportPaths=coverage.xml +sonar.test.inclusions=tests/**/*.py +sonar.exclusions=**/__pycache__/**,config/**,.venv/** +sonar.sourceEncoding=UTF-8 From ba6f3cb0262c31127eb819763135943922921866 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 12:20:41 -0500 Subject: [PATCH 04/14] cleanup --- .github/release-drafter.yml | 38 ----------- .github/workflows/codeql.yaml | 67 ------------------- .../{tests.yaml => quality-gate.yaml} | 2 +- .github/workflows/release-drafter.yaml | 11 --- 4 files changed, 1 insertion(+), 117 deletions(-) delete mode 100644 .github/release-drafter.yml delete mode 100644 .github/workflows/codeql.yaml rename .github/workflows/{tests.yaml => quality-gate.yaml} (98%) delete mode 100644 .github/workflows/release-drafter.yaml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 8ff2263..0000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,38 +0,0 @@ -categories: - - title: ":boom: Breaking Changes" - label: "breaking" - - title: ":rocket: Features" - label: "enhancement" - - title: ":fire: Removals and Deprecations" - label: "removal" - - title: ":beetle: Fixes" - label: "bug" - - title: ":racehorse: Performance" - label: "performance" - - title: ":rotating_light: Testing" - label: "testing" - - title: ":construction_worker: Continuous Integration" - label: "ci" - - title: ":books: Documentation" - label: "documentation" - - title: ":hammer: Refactoring" - label: "refactoring" - - title: ":lipstick: Style" - label: "style" - - title: ":package: Dependencies" - labels: - - "dependencies" - - "build" -template: | - ## Check list (to be removed before releasing) - - [ ] Check stable version reference in documentation is updated - [ ] Version in custom_components/aerogarden/manifest.json has been updated - - ## Overview of changes - - This section highlights the main changes brought by this release. - - ## Changes - - $CHANGES diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml deleted file mode 100644 index 7ba9840..0000000 --- a/.github/workflows/codeql.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - pull_request: - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/tests.yaml b/.github/workflows/quality-gate.yaml similarity index 98% rename from .github/workflows/tests.yaml rename to .github/workflows/quality-gate.yaml index 4072e39..4d7fac3 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/quality-gate.yaml @@ -1,4 +1,4 @@ -name: Tests +name: Quality Gate on: push: diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml deleted file mode 100644 index 6434f0e..0000000 --- a/.github/workflows/release-drafter.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: Release Drafter -on: - workflow_dispatch: - -jobs: - draft_release: - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@v5.13.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4d6baab921b1fb5c23c434f9eae71e9f764e84ee Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 12:33:35 -0500 Subject: [PATCH 05/14] cloud --- .github/workflows/quality-gate.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/quality-gate.yaml b/.github/workflows/quality-gate.yaml index 4d7fac3..2d52ba0 100644 --- a/.github/workflows/quality-gate.yaml +++ b/.github/workflows/quality-gate.yaml @@ -27,10 +27,8 @@ jobs: uses: SonarSource/sonarqube-scan-action@v5.2.0 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - name: SonarQube Quality Gate uses: SonarSource/sonarqube-quality-gate-action@v1.2.0 timeout-minutes: 5 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} From 2ec0fbd46b4eed841f634504682b6c3072669a8b Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 12:41:18 -0500 Subject: [PATCH 06/14] sonar --- .github/workflows/{quality-gate.yaml => quality.yaml} | 4 ++-- .sonarlint/connectedMode.json | 5 +++++ .vscode/settings.json | 6 +++++- sonar-project.properties | 5 +++-- 4 files changed, 15 insertions(+), 5 deletions(-) rename .github/workflows/{quality-gate.yaml => quality.yaml} (96%) create mode 100644 .sonarlint/connectedMode.json diff --git a/.github/workflows/quality-gate.yaml b/.github/workflows/quality.yaml similarity index 96% rename from .github/workflows/quality-gate.yaml rename to .github/workflows/quality.yaml index 2d52ba0..836571c 100644 --- a/.github/workflows/quality-gate.yaml +++ b/.github/workflows/quality.yaml @@ -1,11 +1,11 @@ -name: Quality Gate +name: Quality Check on: push: pull_request: jobs: - test: + quality-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.sonarlint/connectedMode.json b/.sonarlint/connectedMode.json new file mode 100644 index 0000000..959fba9 --- /dev/null +++ b/.sonarlint/connectedMode.json @@ -0,0 +1,5 @@ +{ + "sonarCloudOrganization": "dalinicus", + "projectKey": "homeassistant-acinfinity", + "region": "EU" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b38853..f36b481 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,9 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "sonarlint.connectedMode.project": { + "connectionId": "dalinicus", + "projectKey": "homeassistant-acinfinity" + } } \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index de39b38..aa35dd6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,9 +1,10 @@ -sonar.projectKey=dalinicus_homeassistant-acinfinity +sonar.projectKey=dhomeassistant-acinfinity sonar.projectName=homeassistant-acinfinity +sonar.organization=dalinicus sonar.sources=custom_components sonar.tests=tests sonar.python.version=3.13 sonar.python.coverage.reportPaths=coverage.xml sonar.test.inclusions=tests/**/*.py sonar.exclusions=**/__pycache__/**,config/**,.venv/** -sonar.sourceEncoding=UTF-8 +sonar.sourceEncoding=UTF-8 \ No newline at end of file From 136cd164ae90acdf5b8ccf797fc0af7a234495d8 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 12:46:04 -0500 Subject: [PATCH 07/14] bad key --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index aa35dd6..0674810 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ -sonar.projectKey=dhomeassistant-acinfinity +sonar.projectKey=homeassistant-acinfinity sonar.projectName=homeassistant-acinfinity sonar.organization=dalinicus sonar.sources=custom_components From 8071962538893d8e1ccb7459376f82ee5347d028 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:10:08 -0500 Subject: [PATCH 08/14] more --- .github/workflows/quality-gate.yaml | 40 +++++++++++++++++++++++++++++ .github/workflows/tests.yaml | 34 ++++++++++++++++++++++++ DEVELOPMENT.md | 4 +-- README.md | 2 -- qodana.yaml | 10 ++++++++ sonar-project.properties | 10 -------- 6 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/quality-gate.yaml create mode 100644 .github/workflows/tests.yaml create mode 100644 qodana.yaml delete mode 100644 sonar-project.properties diff --git a/.github/workflows/quality-gate.yaml b/.github/workflows/quality-gate.yaml new file mode 100644 index 0000000..bf0a4de --- /dev/null +++ b/.github/workflows/quality-gate.yaml @@ -0,0 +1,40 @@ +name: Quality Gate +on: + workflow_dispatch: + pull_request: + push: + branches: # Specify your branches here + - main # The 'main' branch + - 'releases/*' # The release branches + +jobs: + quality-gate: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + - uses: actions/setup-python@v5 + name: Setup Python + with: + python-version: 3.13 + - name: Update pip + run: python -m pip install --upgrade pip + - name: Install dependencies + run: pip install --upgrade -r requirements.txt + - name: Run Unit Tests + run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term + - name: Run Type Checks + run: pyright + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2025.3 + with: + args: --coverage-dir,${{ github.workspace }} + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} + QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..2d52ba0 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,34 @@ +name: Quality Gate + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + name: Setup Python + with: + python-version: 3.13 + - name: update pip + run: python -m pip install --upgrade pip + - name: install dependencies + run: pip install --upgrade -r requirements.txt + - name: Run Unit Tests + run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term + - name: Run Type Checks + run: pyright + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v5.2.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: SonarQube Quality Gate + uses: SonarSource/sonarqube-quality-gate-action@v1.2.0 + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 81d12df..5dbc144 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -307,7 +307,7 @@ pytest -v GitHub Actions runs: - `pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term` for testing and coverage collection -- SonarQube scan for static analysis -- SonarQube quality gate to block PRs when conditions are not met +- `pyright` for type checking +- Qodana scan and quality gate to block PRs when conditions are not met Access Home Assistant at `http://localhost:8123` diff --git a/README.md b/README.md index a1030ad..8a314ed 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # homeassistant-acinfinity [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=dalinicus_homeassistant-acinfinity&metric=coverage)](https://sonarcloud.io/summary/new_code?id=dalinicus_homeassistant-acinfinity) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dalinicus_homeassistant-acinfinity&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=dalinicus_homeassistant-acinfinity) [![Tests](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/tests.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/tests.yaml) [![HACS/HASS](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/validate.yaml/badge.svg)](https://github.com/dalinicus/homeassistant-acinfinity/actions/workflows/validate.yaml) diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..38b5eaf --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,10 @@ +#################################################################################################################### +# WARNING: Do not store sensitive information in this file, as its contents will be included in the Qodana report. # +#################################################################################################################### + +version: "1.0" +linter: jetbrains/qodana-python:2025.3 +profile: + name: qodana.recommended +include: + - name: CheckDependencyLicenses \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index 0674810..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,10 +0,0 @@ -sonar.projectKey=homeassistant-acinfinity -sonar.projectName=homeassistant-acinfinity -sonar.organization=dalinicus -sonar.sources=custom_components -sonar.tests=tests -sonar.python.version=3.13 -sonar.python.coverage.reportPaths=coverage.xml -sonar.test.inclusions=tests/**/*.py -sonar.exclusions=**/__pycache__/**,config/**,.venv/** -sonar.sourceEncoding=UTF-8 \ No newline at end of file From 59dce9e82810627010ac901b6ab308a9625e8e96 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:17:56 -0500 Subject: [PATCH 09/14] what happen --- .github/workflows/quality-gate.yaml | 40 ------------------- .github/workflows/quality.yaml | 62 ++++++++++++++++------------- 2 files changed, 34 insertions(+), 68 deletions(-) delete mode 100644 .github/workflows/quality-gate.yaml diff --git a/.github/workflows/quality-gate.yaml b/.github/workflows/quality-gate.yaml deleted file mode 100644 index bf0a4de..0000000 --- a/.github/workflows/quality-gate.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: Quality Gate -on: - workflow_dispatch: - pull_request: - push: - branches: # Specify your branches here - - main # The 'main' branch - - 'releases/*' # The release branches - -jobs: - quality-gate: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - checks: write - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit - fetch-depth: 0 # a full history is required for pull request analysis - - uses: actions/setup-python@v5 - name: Setup Python - with: - python-version: 3.13 - - name: Update pip - run: python -m pip install --upgrade pip - - name: Install dependencies - run: pip install --upgrade -r requirements.txt - - name: Run Unit Tests - run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term - - name: Run Type Checks - run: pyright - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2025.3 - with: - args: --coverage-dir,${{ github.workspace }} - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} - QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 836571c..bf0a4de 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -1,34 +1,40 @@ -name: Quality Check - +name: Quality Gate on: - push: + workflow_dispatch: pull_request: + push: + branches: # Specify your branches here + - main # The 'main' branch + - 'releases/*' # The release branches jobs: - quality-check: + quality-gate: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-python@v5 - name: Setup Python - with: - python-version: 3.13 - - name: update pip - run: python -m pip install --upgrade pip - - name: install dependencies - run: pip install --upgrade -r requirements.txt - - name: Run Unit Tests - run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term - - name: Run Type Checks - run: pyright - - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v5.2.0 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: SonarQube Quality Gate - uses: SonarSource/sonarqube-quality-gate-action@v1.2.0 - timeout-minutes: 5 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + - uses: actions/setup-python@v5 + name: Setup Python + with: + python-version: 3.13 + - name: Update pip + run: python -m pip install --upgrade pip + - name: Install dependencies + run: pip install --upgrade -r requirements.txt + - name: Run Unit Tests + run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term + - name: Run Type Checks + run: pyright + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2025.3 + with: + args: --coverage-dir,${{ github.workspace }} + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} + QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file From f503cfeb3fea8ee2a154be1300de4c13a96465da Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:18:40 -0500 Subject: [PATCH 10/14] stop it --- .github/workflows/tests.yaml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/tests.yaml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 2d52ba0..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Quality Gate - -on: - push: - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-python@v5 - name: Setup Python - with: - python-version: 3.13 - - name: update pip - run: python -m pip install --upgrade pip - - name: install dependencies - run: pip install --upgrade -r requirements.txt - - name: Run Unit Tests - run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term - - name: Run Type Checks - run: pyright - - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v5.2.0 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: SonarQube Quality Gate - uses: SonarSource/sonarqube-quality-gate-action@v1.2.0 - timeout-minutes: 5 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 628cfc83e83d9f6ebde8af4bc27948a85d36add1 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:20:00 -0500 Subject: [PATCH 11/14] d --- .github/workflows/quality.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index bf0a4de..41885c1 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -1,4 +1,4 @@ -name: Quality Gate +name: Code Quality on: workflow_dispatch: pull_request: From 7d8e40256951a812b99161acde93cb0fc5db9e26 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:29:30 -0500 Subject: [PATCH 12/14] remove black, codespell, and ruff --- .github/workflows/quality.yaml | 2 +- DEVELOPMENT.md | 11 ++--- pyproject.toml | 89 ---------------------------------- requirements.txt | 3 -- 4 files changed, 5 insertions(+), 100 deletions(-) diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 41885c1..186cb2e 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -34,7 +34,7 @@ jobs: - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2025.3 with: - args: --coverage-dir,${{ github.workspace }} + args: --coverage-dir ${{ github.workspace }} env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 5dbc144..dc3dd1e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -50,8 +50,7 @@ The integration polls the AC Infinity cloud API and exposes device state, enviro ### IDE Configuration - Use VS Code with Python extension -- Configure mypy for strict type checking -- Enable ruff and black formatters +- Configure pyright for type checking - Set up pytest for test running ### Bruno API Collection Setup @@ -223,11 +222,9 @@ Note: Callback signatures vary by entity scope: ### Style Guidelines -- **Quotes**: Single quotes preferred (`ruff Q000`) -- **Linting**: `ruff` as primary linter with custom rules in `pyproject.toml` -- **Formatting**: `black` for consistent formatting -- **Type Hints**: Strict mypy checking with Python 3.13 type annotations -- **Imports**: Organized and linted automatically +- **Formatting**: Follow the existing code style in the repository +- **Type Hints**: `pyright` checking with Python 3.13 type annotations +- **Imports**: Keep imports organized and readable ### Naming Conventions diff --git a/pyproject.toml b/pyproject.toml index 5b03aae..9c50272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,89 +1,3 @@ -[tool.ruff] -lint.select = [ - "B002", # Python does not support the unary prefix increment - "B007", # Loop control variable {name} not used within loop body - "B014", # Exception handler with duplicate exception - "B023", # Function definition does not bind loop variable {name} - "B026", # Star-arg unpacking after a keyword argument is strongly discouraged - "C", # complexity - "COM818", # Trailing comma on bare tuple prohibited - "E", # pycodestyle - "F", # pyflakes/autoflake - "G", # flake8-logging-format - "I", # isort - "ICN001", # import concentions; {name} should be imported as {asname} - "ISC001", # Implicitly concatenated string literals on one line - "N804", # First argument of a class method should be named cls - "N805", # First argument of a method should be named self - "N815", # Variable {name} in class scope should not be mixedCase - "S307", # No builtin eval() allowed - "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint - "Q000", # Double quotes found but single quotes preferred - "RUF006", # Store a reference to the return value of asyncio.create_task - "S102", # Use of exec detected - "S103", # bad-file-permissions - "S108", # hardcoded-temp-file - "S306", # suspicious-mktemp-usage - "S307", # suspicious-eval-usage - "S313", # suspicious-xmlc-element-tree-usage - "S314", # suspicious-xml-element-tree-usage - "S315", # suspicious-xml-expat-reader-usage - "S316", # suspicious-xml-expat-builder-usage - "S317", # suspicious-xml-sax-usage - "S318", # suspicious-xml-mini-dom-usage - "S319", # suspicious-xml-pull-dom-usage - "S601", # paramiko-call - "S602", # subprocess-popen-with-shell-equals-true - "S604", # call-with-shell-equals-true - "S608", # hardcoded-sql-expression - "S609", # unix-command-wildcard-injection - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM208", # Use {expr} instead of not (not {expr}) - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block - "T100", # Trace found: {name} used - "T20", # flake8-print - "TID251", # Banned imports - "TRY004", # Prefer TypeError exception for invalid type - "B904", # Use raise from to specify exception cause - "TRY203", # Remove exception handler; error is immediately re-raised - "UP", # pyupgrade - "W", # pycodestyle -] - -lint.ignore = [ - "D202", # No blank lines allowed after function docstring - "D203", # 1 blank line required before class docstring - "D213", # Multi-line docstring summary should start at the second line - "D406", # Section name should end with a newline - "D407", # Section name underlining - "E501", # line too long - "E731", # do not assign a lambda expression, use a def - "PLC1901", # Lots of false positives - # False positives https://github.com/astral-sh/ruff/issues/5386 - "PLC0208", # Use a sequence type instead of a `set` when iterating over values - "PLR0911", # Too many return statements ({returns} > {max_returns}) - "PLR0912", # Too many branches ({branches} > {max_branches}) - "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) - "PLR0915", # Too many statements ({statements} > {max_statements}) - "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable - "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target - "UP006", # keep type annotation style as is - "UP007", # keep type annotation style as is - # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 - "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` - -] - [tool.pytest.ini_options] testpaths = [ "tests" @@ -93,9 +7,6 @@ filterwarnings = [ ] asyncio_default_fixture_loop_scope = "function" -[tool.codespell] -skip = "custom_components/ac_infinity/translations/*" - [tool.pyright] pythonVersion = "3.13" typeCheckingMode = "basic" diff --git a/requirements.txt b/requirements.txt index 28fe64c..06bbd9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,6 @@ pytest-mock pytest-cov pytest-asyncio aioresponses -black -codespell -ruff aioresponses pyright python-dateutil From e90eac745912a35adcd0350195367f33c48e8642 Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:44:49 -0500 Subject: [PATCH 13/14] hows this? --- .github/workflows/quality.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 186cb2e..3ecf38a 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + ref: ${{ github.event.pull_request.head.sha || github.sha }} # use the PR head commit when available, otherwise the push SHA fetch-depth: 0 # a full history is required for pull request analysis - uses: actions/setup-python@v5 name: Setup Python @@ -27,14 +27,16 @@ jobs: run: python -m pip install --upgrade pip - name: Install dependencies run: pip install --upgrade -r requirements.txt + - name: Prepare Qodana coverage directory + run: mkdir -p .qodana/code-coverage - name: Run Unit Tests - run: pytest --cov=custom_components/ac_infinity --cov-report=xml --cov-report=term + run: pytest --cov=custom_components/ac_infinity --cov-report=xml:.qodana/code-coverage/coverage.xml --cov-report=term - name: Run Type Checks run: pyright - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2025.3 with: - args: --coverage-dir ${{ github.workspace }} + args: --coverage-dir .qodana/code-coverage env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file From 1a35d4e90c18c98c7a0280bce6da42dfabe4fcfd Mon Sep 17 00:00:00 2001 From: Ryan Mattson Date: Mon, 30 Mar 2026 13:55:23 -0500 Subject: [PATCH 14/14] this? --- .github/workflows/quality.yaml | 18 +++++++++++++----- requirements.txt | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 3ecf38a..8ac8001 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -27,16 +27,24 @@ jobs: run: python -m pip install --upgrade pip - name: Install dependencies run: pip install --upgrade -r requirements.txt - - name: Prepare Qodana coverage directory - run: mkdir -p .qodana/code-coverage - name: Run Unit Tests - run: pytest --cov=custom_components/ac_infinity --cov-report=xml:.qodana/code-coverage/coverage.xml --cov-report=term + run: | + mkdir -p .qodana/code-coverage + coverage run -m pytest + coverage xml -o .qodana/code-coverage/coverage.xml - name: Run Type Checks run: pyright + - name: Verify coverage report exists + run: | + test -f .qodana/code-coverage/coverage.xml + wc -c .qodana/code-coverage/coverage.xml + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: qodana-coverage-data + path: .qodana/code-coverage - name: 'Qodana Scan' uses: JetBrains/qodana-action@v2025.3 - with: - args: --coverage-dir .qodana/code-coverage env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 06bbd9f..e674ffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,8 +5,8 @@ DateTime async-timeout pytest pytest-mock -pytest-cov pytest-asyncio +coverage aioresponses aioresponses pyright