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/.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/quality.yaml b/.github/workflows/quality.yaml new file mode 100644 index 0000000..8ac8001 --- /dev/null +++ b/.github/workflows/quality.yaml @@ -0,0 +1,50 @@ +name: Code Quality +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 || 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 + 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: | + 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 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_810067939 }} + QODANA_ENDPOINT: 'https://qodana.cloud' \ No newline at end of file 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 }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 66a6eb6..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Tests - -on: - push: - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - 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 - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} 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 new file mode 100644 index 0000000..f36b481 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "sonarlint.connectedMode.project": { + "connectionId": "dalinicus", + "projectKey": "homeassistant-acinfinity" + } +} \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index e50c998..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 @@ -279,8 +276,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 +303,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 +- `pyright` for type checking +- Qodana scan and 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..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) - -[![codecov](https://codecov.io/gh/dalinicus/homeassistant-acinfinity/graph/badge.svg?token=C4TMDAU344)](https://codecov.io/gh/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/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/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/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..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" @@ -26,11 +40,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..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: @@ -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/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..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,12 +7,7 @@ filterwarnings = [ ] 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/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/requirements.txt b/requirements.txt index 2f415f2..e674ffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,13 +5,10 @@ DateTime async-timeout pytest pytest-mock -pytest-cov pytest-asyncio +coverage aioresponses -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/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..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="", @@ -213,12 +214,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() @@ -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( @@ -399,19 +400,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