diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index f3adfb1..1ee9922 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -1,7 +1,6 @@ ---- - name: Validate with hassfest +# yamllint disable-line rule:truthy on: push: pull_request: diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index bdad7d1..b0e3f19 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -1,30 +1,15 @@ -name: Linting +name: pre-commit +# yamllint disable-line rule:truthy on: - push: - branches: - - main - - master - - dev pull_request: + push: + branches: [master] jobs: pre-commit: runs-on: ubuntu-latest - name: pre-commit steps: - - uses: actions/checkout@v3.1.0 - - uses: actions/setup-python@v4.3.0 - with: - python-version: "3.10" - - run: | - pip install --constraint=.github/workflows/constraints.txt pip - pip install --constraint=.github/workflows/constraints.txt pre-commit - - uses: actions/cache@v3.0.11 - if: matrix.os != 'windows-latest' - with: - path: ~/.cache/pre-commit - key: ${{ steps.cache_key_prefix.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - ${{ steps.cache_key_prefix.outputs.result }}- - - run: pre-commit run --all-files --show-diff-on-failure --color=always + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3726d13..100fe88 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,16 @@ ---- - repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.1 hooks: - id: pyupgrade - args: [ --py310 ] + args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.1.0 hooks: - id: black args: - - --safe - --quiet - files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ + files: ^((custom_components)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v2.2.2 hooks: @@ -23,12 +20,26 @@ repos: - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] - exclude: ^tests/fixtures/ - - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.1 + hooks: + - id: autoflake + args: + - --in-place + - --remove-all-unused-imports + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 hooks: - id: flake8 - files: ^(homeassistant|script|tests)/.+\.py$ + additional_dependencies: + - pycodestyle==2.10.0 + - pyflakes==3.0.1 + # - flake8-docstrings==1.6.0 + # - pydocstyle==6.2.3 + - flake8-comprehensions==3.10.1 + - flake8-noqa==1.3.0 + - mccabe==0.7.0 + files: ^(custom_components)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.7.4 hooks: @@ -36,24 +47,26 @@ repos: args: - --quiet - --format=custom - files: ^(homeassistant|script|tests)/.+\.py$ + - --configfile=bandit.yaml + files: ^(custom_components)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.11.5 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - - id: check-executables-have-shebangs - stages: [manual] - id: check-json exclude: (.vscode|.devcontainer) - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - id: check-added-large-files + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.28.0 + hooks: + - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.1 + rev: v3.0.0-alpha.4 hooks: - id: prettier - stages: [manual] diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..c2f877a --- /dev/null +++ b/.yamllint @@ -0,0 +1,61 @@ +ignore: | + azure-*.yml +rules: + braces: + level: error + min-spaces-inside: 0 + max-spaces-inside: 1 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + brackets: + level: error + min-spaces-inside: 0 + max-spaces-inside: 0 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + colons: + level: error + max-spaces-before: 0 + max-spaces-after: 1 + commas: + level: error + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + comments: + level: error + require-starting-space: true + min-spaces-from-content: 2 + comments-indentation: + level: error + document-end: + level: error + present: false + document-start: + level: error + present: false + empty-lines: + level: error + max: 1 + max-start: 0 + max-end: 1 + hyphens: + level: error + max-spaces-after: 1 + indentation: + level: error + spaces: 2 + indent-sequences: true + check-multi-line-strings: false + key-duplicates: + level: error + line-length: disable + new-line-at-end-of-file: + level: error + new-lines: + level: error + type: unix + trailing-spaces: + level: error + truthy: + level: error diff --git a/CHANGELOG.md b/CHANGELOG.md index b1dcfab..01c12e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.0.26 + +Configuration breaking change + +- Change units from Bytes, KBytes and MBytes to B, KB and MB, if the configured unit was other than Bytes, please re-configure it +- Last activity as seconds to return without milliseconds +- Upgrade pre-commit-configuration by [@tetienne](https://github.com/tetienne) [PR #91](https://github.com/elad-bar/ha-edgeos/pull/91) +- Fix integration reload after changing configuration + ## 2.0.25 - Add support for Home Assistant integration and device diagnostics @@ -30,7 +39,7 @@ **Version requires HA v2022.11.0 and above** -- Aligned *Core Select* according to new HA *SelectEntityDescription* object +- Aligned _Core Select_ according to new HA _SelectEntityDescription_ object ## 2.0.20 @@ -66,12 +75,13 @@ For now, special interface do not support to turn on / off ## 2.0.14 **Debugging became easier (less IO and Disk Space)** + - Removed `Store Debug Data` switch (Moved to the API endpoints below) - Removed WebSocket messages sensors (Moved to the API endpoints below) - Add endpoints to expose the data was previously stored to files and the messages counters | Endpoint Name | Method | Description | -|----------------------------|--------|-----------------------------------------------------------------------------------------------------| +| -------------------------- | ------ | --------------------------------------------------------------------------------------------------- | | /api/edgeos/list | GET | List all the endpoints available (supporting multiple integrations), available once for integration | | /api/edgeos/{ENTRY_ID}/ha | GET | JSON of all HA processed data before sent to entities including messages counters, per integration | | /api/edgeos/{ENTRY_ID}/api | GET | JSON of all raw data from the EdgeOS API, per integration | @@ -144,9 +154,11 @@ For now, special interface do not support to turn on / off - Fix missing validation of entry ## 2.0.0 + Component refactored to allow faster future integration for additional features. New features: + - Enable / Disable interface (Ethernet / Bridge) using a new switch per interface - Enable / Disable interface monitoring for received and sent data / rate / errors / packets and dropped packets using a switch per interface - Enable / Disable device monitoring for received and sent data and rate (including device tracker) using a switch per interface @@ -157,6 +169,7 @@ New features: - New service: `Update configuration` allows to edit configuration of unit, store debug data, log incoming messages and consider away interval **Breaking Changes!** + - Most of the configurations moved to be regular components of HA (Log incoming messages, Unit of measurement, Store debug data) - Configuration UI will hold EdgeOS URL and credentials only: - Hostname @@ -167,7 +180,7 @@ New features: **System** | Entity Name | Type | Description | Additional information | -|-------------------------------------|---------------|---------------------------------------------------------------------------|-----------------------------------------------| +| ----------------------------------- | ------------- | ------------------------------------------------------------------------- | --------------------------------------------- | | {Router Name} Unit | Select | Sets whether to monitor device and create all the components below or not | | | {Router Name} Unknown devices | Sensor | Represents number of devices leased by the DHCP server | Attributes holds the leased hostname and IPs | | {Router Name} CPU | Sensor | Represents CPU usage | Attributes holds the leased hostname and IPs | @@ -177,11 +190,10 @@ New features: | {Router Name} Log incoming messages | Switch | Sets whether to log WebSocket incoming messages for debugging | | | {Router Name} Store Debug Data | Switch | Sets whether to store API and WebSocket latest data for debugging | | - **Per device** | Entity Name | Type | Description | Additional information | -|----------------------------------------------|----------------|---------------------------------------------------------------------------------|-----------------------------| +| -------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | --------------------------- | | {Router Name} {Device Name} Monitored | Sensor | Sets whether to monitor device and create all the components below or not | | | {Router Name} {Device Name} Received Rate | Sensor | Received Rate per second | Statistics: Measurement | | {Router Name} {Device Name} Received Traffic | Sensor | Received total traffic | Statistics: Total Increment | @@ -189,11 +201,10 @@ New features: | {Router Name} {Device Name} Sent Traffic | Sensor | Sent total traffic | Statistics: Total Increment | | {Router Name} {Device Name} | Device Tracker | Indication whether the device is or was connected over the configured timeframe | | - **Per interface** | Entity Name | Type | Description | Additional information | -|---------------------------------------------------------|--------|------------------------------------------------------------------------------|-----------------------------| +| ------------------------------------------------------- | ------ | ---------------------------------------------------------------------------- | --------------------------- | | {Router Name} {Interface Name} Status | Switch | Sets whether to interface is active or not | | | {Router Name} {Interface Name} Monitored | Switch | Sets whether to monitor interface and create all the components below or not | | | {Router Name} {Interface Name} Received Rate | Sensor | Received Rate per second | Statistics: Measurement | @@ -207,7 +218,6 @@ New features: | {Router Name} {Interface Name} Sent Errors | Sensor | Sent errors | Statistics: Total Increment | | {Router Name} {Interface Name} Sent Packets | Sensor | Sent packets | Statistics: Total Increment | - ## 1.2.6 - Restore value exception handling for WebSocket diff --git a/README.md b/README.md index df53797..b94c198 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ Provides an integration between EdgeOS (Ubiquiti) routers to Home Assistant. - To enable / disable interfaces an `admin` role is a required #### Installations via HACS + - In HACS, look for "Ubiquiti EdgeOS Routers" and install and restart -- In Settings --> Devices & Services - (Lower Right) "Add Integration" +- In Settings --> Devices & Services - (Lower Right) "Add Integration" #### Setup @@ -25,7 +26,7 @@ To add integration use Configuration -> Integrations -> Add `EdgeOS` Integration supports **multiple** EdgeOS devices | Fields name | Type | Required | Default | Description | -|-------------|---------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| ----------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Host | Textbox | + | - | Hostname or IP address to access EdgeOS device, can hold also port (HOST:PORT), default port is 443 | | Username | Textbox | + | - | Username of user with `Operator` level access or higher, better to create a dedicated user for that integration for faster issues identification | | Password | Textbox | + | - | | @@ -33,7 +34,7 @@ Integration supports **multiple** EdgeOS devices ###### EdgeOS Device validation errors | Errors | -|------------------------------------------------------------------------------------| +| ---------------------------------------------------------------------------------- | | Cannot reach device (404) | | Invalid credentials (403) | | General authentication error (when failed to get valid response from device) | @@ -58,7 +59,7 @@ Please remove the integration and re-add it to make it work again. _Configuration -> Integrations -> {Integration} -> Options_
| Fields name | Type | Required | Default | Description | -|-------------------|-----------|----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| ----------------- | --------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Host | Textbox | + | - | Hostname or IP address to access EdgeOS device, can hold also port (HOST:PORT), default port is 443 | | Username | Textbox | + | - | Username of user with `Operator` level access or higher, better to create a dedicated user for that integration for faster issues identification | | Password | Textbox | + | - | | @@ -78,8 +79,9 @@ logger: ## Components ### System + | Entity Name | Type | Description | Additional information | -|-------------------------------------|---------------|---------------------------------------------------------------------------|-----------------------------------------------| +| ----------------------------------- | ------------- | ------------------------------------------------------------------------- | --------------------------------------------- | | {Router Name} Unit | Select | Sets whether to monitor device and create all the components below or not | | | {Router Name} CPU | Sensor | Represents CPU usage | | | {Router Name} RAM | Sensor | Represents RAM usage | | @@ -88,11 +90,12 @@ logger: | {Router Name} Firmware Updates | Binary Sensor | New firmware available indication | Attributes holds the url and new release name | | {Router Name} Log incoming messages | Switch | Sets whether to log WebSocket incoming messages for debugging | | -*Changing the unit will reload the integration* +_Changing the unit will reload the integration_ ### Per device + | Entity Name | Type | Description | Additional information | -|----------------------------------------------|----------------|---------------------------------------------------------------------------------|-----------------------------| +| -------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | --------------------------- | | {Router Name} {Device Name} Monitored | Sensor | Sets whether to monitor device and create all the components below or not | | | {Router Name} {Device Name} Received Rate | Sensor | Received Rate per second | Statistics: Measurement | | {Router Name} {Device Name} Received Traffic | Sensor | Received total traffic | Statistics: Total Increment | @@ -100,10 +103,10 @@ logger: | {Router Name} {Device Name} Sent Traffic | Sensor | Sent total traffic | Statistics: Total Increment | | {Router Name} {Device Name} | Device Tracker | Indication whether the device is or was connected over the configured timeframe | | - ### Per interface + | Entity Name | Type | Description | Additional information | -|---------------------------------------------------------|---------------|------------------------------------------------------------------------------|---------------------------------------------| +| ------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------- | ------------------------------------------- | | {Router Name} {Interface Name} Status | Switch | Sets whether to interface is active or not | Available only if user level is `admin` | | {Router Name} {Interface Name} Status | Binary Sensor | Indicates whether interface is active or not | Available only if user level is not `admin` | | {Router Name} {Interface Name} Connected | Binary Sensor | Indicates whether interface's port is connected or not | | @@ -119,13 +122,14 @@ logger: | {Router Name} {Interface Name} Sent Errors | Sensor | Sent errors | Statistics: Total Increment | | {Router Name} {Interface Name} Sent Packets | Sensor | Sent packets | Statistics: Total Increment | - _Unit of measurement for `Traffic` and `Rate` are according to the unit settings of the integration_ ## Services ### Update configuration + Allows to set: + - Consider away interval - Time to consider a device without activity as AWAY (any value between 10 and 1800 in seconds) - Log incoming messages - Enable / Disable logging of incoming WebSocket messages for debug - Store debug data - Enable / Disable store debug data to './storage' directory of HA for API (edgeos.debug.api.json) and WS (edgeos.debug.ws.json) data for faster debugging or just to get more ideas for additional features @@ -138,7 +142,7 @@ More details available in `Developer tools` -> `Services` -> `edgeos.update_conf ```yaml service: edgeos.update_configuration data: - device_id: {Main device ID} + device_id: { Main device ID } unit: Bytes log_incoming_messages: true consider_away_interval: 180 @@ -146,4 +150,4 @@ data: update_entities_interval: 1 ``` -*Changing the unit will reload the integration* +_Changing the unit will reload the integration_ diff --git a/bandit.yaml b/bandit.yaml new file mode 100644 index 0000000..568f77d --- /dev/null +++ b/bandit.yaml @@ -0,0 +1,21 @@ +# https://bandit.readthedocs.io/en/latest/config.html + +tests: + - B103 + - B108 + - B306 + - B307 + - B313 + - B314 + - B315 + - B316 + - B317 + - B318 + - B319 + - B320 + - B325 + - B601 + - B602 + - B604 + - B608 + - B609 diff --git a/custom_components/edgeos/__init__.py b/custom_components/edgeos/__init__.py index 002700e..d4f4491 100644 --- a/custom_components/edgeos/__init__.py +++ b/custom_components/edgeos/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from .component.helpers import async_set_ha, clear_ha, get_ha -from .component.helpers.const import * +from .configuration.helpers.const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/edgeos/binary_sensor.py b/custom_components/edgeos/binary_sensor.py index 749fa9f..baa2bd1 100644 --- a/custom_components/edgeos/binary_sensor.py +++ b/custom_components/edgeos/binary_sensor.py @@ -14,12 +14,18 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the component.""" await async_setup_base_entry( - hass, config_entry, async_add_devices, CoreBinarySensor.get_domain(), CoreBinarySensor.get_component + hass, + config_entry, + async_add_devices, + CoreBinarySensor.get_domain(), + CoreBinarySensor.get_component, ) async def async_unload_entry(hass, config_entry): - _LOGGER.info(f"Unload entry for {CoreBinarySensor.get_domain()} domain: {config_entry}") + _LOGGER.info( + f"Unload entry for {CoreBinarySensor.get_domain()} domain: {config_entry}" + ) return True diff --git a/custom_components/edgeos/component/api/api.py b/custom_components/edgeos/component/api/api.py index 9613ea5..6bf8dbb 100644 --- a/custom_components/edgeos/component/api/api.py +++ b/custom_components/edgeos/component/api/api.py @@ -2,19 +2,57 @@ from asyncio import sleep from collections.abc import Awaitable, Callable -from datetime import datetime +from datetime import datetime, timedelta import json import logging import sys from aiohttp import CookieJar +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from ...configuration.helpers.const import ( + COOKIE_BEAKER_SESSION_ID, + COOKIE_CSRF_TOKEN, + COOKIE_PHPSESSID, + HEADER_CSRF_TOKEN, + MAXIMUM_RECONNECT, +) from ...configuration.models.config_data import ConfigData from ...core.api.base_api import BaseAPI +from ...core.helpers.const import EMPTY_STRING from ...core.helpers.enums import ConnectivityStatus -from ..helpers.const import * +from ..helpers.const import ( + API_DATA, + API_DATA_COOKIES, + API_DATA_INTERFACES, + API_DATA_LAST_UPDATE, + API_DATA_PRODUCT, + API_DATA_SAVE, + API_DATA_SESSION_ID, + API_DATA_SYSTEM, + API_DELETE, + API_GET, + API_SET, + API_URL_DATA, + API_URL_DATA_SUBSET, + API_URL_HEARTBEAT, + API_URL_PARAMETER_ACTION, + API_URL_PARAMETER_BASE_URL, + API_URL_PARAMETER_SUBSET, + API_URL_PARAMETER_TIMESTAMP, + HEARTBEAT_MAX_AGE, + RESPONSE_ERROR_KEY, + RESPONSE_FAILURE_CODE, + RESPONSE_OUTPUT, + RESPONSE_SUCCESS_KEY, + STRING_DASH, + STRING_UNDERSCORE, + SYSTEM_DATA_DISABLE, + TRUE_STR, + UPDATE_DATE_ENDPOINTS, +) from ..models.edge_os_interface_data import EdgeOSInterfaceData from ..models.exceptions import SessionTerminatedException @@ -26,12 +64,13 @@ class IntegrationAPI(BaseAPI): _config_data: ConfigData | None - def __init__(self, - hass: HomeAssistant | None, - async_on_data_changed: Callable[[], Awaitable[None]] | None = None, - async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] | None = None - ): - + def __init__( + self, + hass: HomeAssistant | None, + async_on_data_changed: Callable[[], Awaitable[None]] | None = None, + async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] + | None = None, + ): super().__init__(hass, async_on_data_changed, async_on_status_changed) try: @@ -45,9 +84,7 @@ def __init__(self, exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error( - f"Failed to load API, error: {ex}, line: {line_number}" - ) + _LOGGER.error(f"Failed to load API, error: {ex}, line: {line_number}") @property def session_id(self): @@ -85,9 +122,7 @@ async def initialize(self, config_data: ConfigData): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error( - f"Failed to initialize API, error: {ex}, line: {line_number}" - ) + _LOGGER.error(f"Failed to initialize API, error: {ex}, line: {line_number}") async def validate(self, data: dict | None = None): config_data = ConfigData.from_dict(data) @@ -125,8 +160,8 @@ async def login(self): response.raise_for_status() logged_in = ( - self.beaker_session_id is not None - and self.beaker_session_id == self.session_id + self.beaker_session_id is not None + and self.beaker_session_id == self.session_id ) if logged_in: @@ -136,7 +171,9 @@ async def login(self): if "EDGE.DeviceModel" in line: line_parts = line.split(" = ") value = line_parts[len(line_parts) - 1] - self.data[API_DATA_PRODUCT] = value.replace("'", EMPTY_STRING) + self.data[API_DATA_PRODUCT] = value.replace( + "'", EMPTY_STRING + ) self.data[API_DATA_SESSION_ID] = self.session_id self.data[API_DATA_COOKIES] = self._cookies @@ -144,7 +181,7 @@ async def login(self): break else: - _LOGGER.error(f"Failed to login, Invalid credentials") + _LOGGER.error("Failed to login, Invalid credentials") if self.beaker_session_id is None and self.session_id is not None: await self.set_status(ConnectivityStatus.Failed) @@ -164,13 +201,13 @@ async def login(self): await self.set_status(ConnectivityStatus.NotFound) - async def _async_get(self, - endpoint, - timestamp: str | None = None, - action: str | None = None, - subset: str | None = None - ): - + async def _async_get( + self, + endpoint, + timestamp: str | None = None, + action: str | None = None, + subset: str | None = None, + ): result = None message = None status = 404 @@ -240,7 +277,9 @@ async def _async_post(self, endpoint, data): headers = self._get_post_headers() data_json = json.dumps(data) - async with self.session.post(url, headers=headers, data=data_json, ssl=False) as response: + async with self.session.post( + url, headers=headers, data=data_json, ssl=False + ) as response: response.raise_for_status() result = await response.json() @@ -264,14 +303,18 @@ async def async_send_heartbeat(self, max_age=HEARTBEAT_MAX_AGE): if current_invocation > timedelta(seconds=max_age): current_ts = str(int(ts.timestamp())) - response = await self._async_get(API_URL_HEARTBEAT, timestamp=current_ts) + response = await self._async_get( + API_URL_HEARTBEAT, timestamp=current_ts + ) if response is not None: _LOGGER.debug(f"Heartbeat response: {response}") self._last_valid = ts else: - _LOGGER.debug(f"Ignoring request to send heartbeat, Reason: closed session") + _LOGGER.debug( + "Ignoring request to send heartbeat, Reason: closed session" + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno @@ -299,7 +342,9 @@ async def async_update(self): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract WS data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract WS data, Error: {ex}, Line: {line_number}" + ) async def _load_system_data(self): try: @@ -314,14 +359,18 @@ async def _load_system_data(self): if success_key == TRUE_STR: if API_GET.upper() in result_json: - self.data[API_DATA_SYSTEM] = result_json.get(API_GET.upper(), {}) + self.data[API_DATA_SYSTEM] = result_json.get( + API_GET.upper(), {} + ) else: error_message = result_json[RESPONSE_ERROR_KEY] _LOGGER.error(f"Failed, Error: {error_message}") else: _LOGGER.error("Invalid response, not contain success status") else: - _LOGGER.debug(f"Ignoring request to get devices data, Reason: closed session") + _LOGGER.debug( + "Ignoring request to get devices data, Reason: closed session" + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno @@ -337,7 +386,9 @@ async def _load_general_data(self, key): clean_item = key.replace(STRING_DASH, STRING_UNDERSCORE) - data = await self._async_get(API_URL_DATA_SUBSET, action=API_DATA, subset=clean_item) + data = await self._async_get( + API_URL_DATA_SUBSET, action=API_DATA, subset=clean_item + ) if data is not None: if RESPONSE_SUCCESS_KEY in data: @@ -348,7 +399,9 @@ async def _load_general_data(self, key): else: self.data[key] = data.get(RESPONSE_OUTPUT) else: - _LOGGER.debug(f"Ignoring request to get data of {key}, Reason: closed session") + _LOGGER.debug( + f"Ignoring request to get data of {key}, Reason: closed session" + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() @@ -356,7 +409,9 @@ async def _load_general_data(self, key): _LOGGER.error(f"Failed to load {key}, Error: {ex}, Line: {line_number}") - async def set_interface_state(self, interface: EdgeOSInterfaceData, is_enabled: bool): + async def set_interface_state( + self, interface: EdgeOSInterfaceData, is_enabled: bool + ): _LOGGER.info(f"Set state of interface {interface.name} to {is_enabled}") modified = False @@ -364,11 +419,7 @@ async def set_interface_state(self, interface: EdgeOSInterfaceData, is_enabled: data = { API_DATA_INTERFACES: { - interface.interface_type: { - interface.name: { - SYSTEM_DATA_DISABLE: None - } - } + interface.interface_type: {interface.name: {SYSTEM_DATA_DISABLE: None}} } } @@ -385,14 +436,22 @@ async def set_interface_state(self, interface: EdgeOSInterfaceData, is_enabled: modified = success_key != RESPONSE_FAILURE_CODE if not modified: - _LOGGER.error(f"Failed to set state of interface {interface.name} to {is_enabled}") + _LOGGER.error( + f"Failed to set state of interface {interface.name} to {is_enabled}" + ) - def _build_endpoint(self, endpoint, timestamp: str | None = None, action: str | None = None, subset: str | None = None): + def _build_endpoint( + self, + endpoint, + timestamp: str | None = None, + action: str | None = None, + subset: str | None = None, + ): data = { API_URL_PARAMETER_BASE_URL: self._config_data.url, API_URL_PARAMETER_TIMESTAMP: timestamp, API_URL_PARAMETER_ACTION: action, - API_URL_PARAMETER_SUBSET: subset + API_URL_PARAMETER_SUBSET: subset, } url = endpoint.format(**data) diff --git a/custom_components/edgeos/component/api/storage_api.py b/custom_components/edgeos/component/api/storage_api.py index b32ff61..2d72a77 100644 --- a/custom_components/edgeos/component/api/storage_api.py +++ b/custom_components/edgeos/component/api/storage_api.py @@ -10,8 +10,23 @@ from ...configuration.models.config_data import ConfigData from ...core.api.base_api import BaseAPI +from ...core.helpers.const import DOMAIN, STORAGE_VERSION from ...core.helpers.enums import ConnectivityStatus -from ..helpers.const import * +from ..helpers.const import ( + ATTR_BYTE, + DEFAULT_CONSIDER_AWAY_INTERVAL, + DEFAULT_UPDATE_API_INTERVAL, + DEFAULT_UPDATE_ENTITIES_INTERVAL, + STORAGE_DATA_CONSIDER_AWAY_INTERVAL, + STORAGE_DATA_FILE_CONFIG, + STORAGE_DATA_FILES, + STORAGE_DATA_LOG_INCOMING_MESSAGES, + STORAGE_DATA_MONITORED_DEVICES, + STORAGE_DATA_MONITORED_INTERFACES, + STORAGE_DATA_UNIT, + STORAGE_DATA_UPDATE_API_INTERVAL, + STORAGE_DATA_UPDATE_ENTITIES_INTERVAL, +) _LOGGER = logging.getLogger(__name__) @@ -21,12 +36,13 @@ class StorageAPI(BaseAPI): _config_data: ConfigData | None _data: dict - def __init__(self, - hass: HomeAssistant | None, - async_on_data_changed: Callable[[], Awaitable[None]] | None = None, - async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] | None = None - ): - + def __init__( + self, + hass: HomeAssistant | None, + async_on_data_changed: Callable[[], Awaitable[None]] | None = None, + async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] + | None = None, + ): super().__init__(hass, async_on_data_changed, async_on_status_changed) self._config_data = None @@ -53,7 +69,7 @@ def monitored_devices(self): @property def unit(self): - result = self.data.get(STORAGE_DATA_UNIT, ATTR_BYTE).replace(ATTR_BYTE[1:], "").upper() + result = self.data.get(STORAGE_DATA_UNIT, ATTR_BYTE) return result @@ -65,19 +81,28 @@ def log_incoming_messages(self): @property def consider_away_interval(self): - result = self.data.get(STORAGE_DATA_CONSIDER_AWAY_INTERVAL, DEFAULT_CONSIDER_AWAY_INTERVAL.total_seconds()) + result = self.data.get( + STORAGE_DATA_CONSIDER_AWAY_INTERVAL, + DEFAULT_CONSIDER_AWAY_INTERVAL.total_seconds(), + ) return result @property def update_entities_interval(self): - result = self.data.get(STORAGE_DATA_UPDATE_ENTITIES_INTERVAL, DEFAULT_UPDATE_ENTITIES_INTERVAL.total_seconds()) + result = self.data.get( + STORAGE_DATA_UPDATE_ENTITIES_INTERVAL, + DEFAULT_UPDATE_ENTITIES_INTERVAL.total_seconds(), + ) return result @property def update_api_interval(self): - result = self.data.get(STORAGE_DATA_UPDATE_API_INTERVAL, DEFAULT_UPDATE_API_INTERVAL.total_seconds()) + result = self.data.get( + STORAGE_DATA_UPDATE_API_INTERVAL, + DEFAULT_UPDATE_API_INTERVAL.total_seconds(), + ) return result @@ -96,7 +121,9 @@ def _initialize_storages(self): for storage_data_file in STORAGE_DATA_FILES: file_name = f"{DOMAIN}.{entry_id}.{storage_data_file}.json" - stores[storage_data_file] = Store(self.hass, STORAGE_VERSION, file_name, encoder=JSONEncoder) + stores[storage_data_file] = Store( + self.hass, STORAGE_VERSION, file_name, encoder=JSONEncoder + ) self._stores = stores @@ -112,7 +139,7 @@ async def _async_load_configuration(self): STORAGE_DATA_LOG_INCOMING_MESSAGES: False, STORAGE_DATA_CONSIDER_AWAY_INTERVAL: DEFAULT_CONSIDER_AWAY_INTERVAL.total_seconds(), STORAGE_DATA_UPDATE_ENTITIES_INTERVAL: DEFAULT_UPDATE_ENTITIES_INTERVAL.total_seconds(), - STORAGE_DATA_UPDATE_API_INTERVAL: DEFAULT_UPDATE_API_INTERVAL.total_seconds() + STORAGE_DATA_UPDATE_API_INTERVAL: DEFAULT_UPDATE_API_INTERVAL.total_seconds(), } await self._async_save() diff --git a/custom_components/edgeos/component/api/websocket.py b/custom_components/edgeos/component/api/websocket.py index c0b1743..5375038 100644 --- a/custom_components/edgeos/component/api/websocket.py +++ b/custom_components/edgeos/component/api/websocket.py @@ -10,12 +10,47 @@ import sys from urllib.parse import urlparse +import aiohttp + from homeassistant.core import HomeAssistant -from ...component.helpers.const import * +from ...configuration.helpers.const import WEBSOCKET_URL_TEMPLATE from ...configuration.models.config_data import ConfigData from ...core.api.base_api import BaseAPI +from ...core.helpers.const import EMPTY_STRING from ...core.helpers.enums import ConnectivityStatus +from ..helpers.const import ( + ADDRESS_HW_ADDR, + ADDRESS_IPV4, + ADDRESS_LIST, + API_DATA_COOKIES, + API_DATA_SESSION_ID, + BEGINS_WITH_SIX_DIGITS, + DEVICE_LIST, + DISCOVER_DEVICE_ITEMS, + INTERFACE_DATA_MULTICAST, + INTERFACES_MAIN_MAP, + INTERFACES_STATS, + STRING_COLON, + STRING_COMMA, + TRAFFIC_DATA_DEVICE_ITEMS, + TRAFFIC_DATA_DIRECTIONS, + TRAFFIC_DATA_INTERFACE_ITEMS, + WS_CLOSING_MESSAGE, + WS_COMPRESSION_DEFLATE, + WS_DISCOVER_KEY, + WS_EXPORT_KEY, + WS_IGNORED_MESSAGES, + WS_INTERFACES_KEY, + WS_MAX_MSG_SIZE, + WS_RECEIVED_MESSAGES, + WS_SESSION_ID, + WS_SYSTEM_STATS_KEY, + WS_TIMEOUT, + WS_TOPIC_NAME, + WS_TOPIC_SUBSCRIBE, + WS_TOPIC_UNSUBSCRIBE, +) _LOGGER = logging.getLogger(__name__) @@ -27,12 +62,13 @@ class IntegrationWS(BaseAPI): _previous_message: dict | None _ws_handlers: dict - def __init__(self, - hass: HomeAssistant | None, - async_on_data_changed: Callable[[], Awaitable[None]] | None = None, - async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] | None = None - ): - + def __init__( + self, + hass: HomeAssistant | None, + async_on_data_changed: Callable[[], Awaitable[None]] | None = None, + async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] + | None = None, + ): super().__init__(hass, async_on_data_changed, async_on_status_changed) self._config_data = None @@ -69,12 +105,12 @@ async def update_api_data(self, api_data: dict, can_log_messages: bool): async def initialize(self, config_data: ConfigData | None = None): if config_data is None: - _LOGGER.debug(f"Reinitializing WebSocket connection") + _LOGGER.debug("Reinitializing WebSocket connection") else: self._config_data = config_data - _LOGGER.debug(f"Initializing WebSocket connection") + _LOGGER.debug("Initializing WebSocket connection") try: self.data = { @@ -90,9 +126,8 @@ async def initialize(self, config_data: ConfigData | None = None): autoclose=True, max_msg_size=WS_MAX_MSG_SIZE, timeout=WS_TIMEOUT, - compress=WS_COMPRESSION_DEFLATE + compress=WS_COMPRESSION_DEFLATE, ) as ws: - await self.set_status(ConnectivityStatus.Connected) self._ws = ws @@ -103,7 +138,7 @@ async def initialize(self, config_data: ConfigData | None = None): except Exception as ex: if self.session is not None and self.session.closed: - _LOGGER.info(f"WS Session closed") + _LOGGER.info("WS Session closed") await self.terminate() @@ -112,10 +147,14 @@ async def initialize(self, config_data: ConfigData | None = None): line_number = tb.tb_lineno if self.status == ConnectivityStatus.Connected: - _LOGGER.info(f"WS got disconnected will try to recover, Error: {ex}, Line: {line_number}") + _LOGGER.info( + f"WS got disconnected will try to recover, Error: {ex}, Line: {line_number}" + ) else: - _LOGGER.warning(f"Failed to connect WS, Error: {ex}, Line: {line_number}") + _LOGGER.warning( + f"Failed to connect WS, Error: {ex}, Line: {line_number}" + ) await self.set_status(ConnectivityStatus.Failed) @@ -132,17 +171,14 @@ async def terminate(self): self._ws = None async def async_send_heartbeat(self): - _LOGGER.debug(f"Keep alive message sent") + _LOGGER.debug("Keep alive message sent") if self.session is None or self.session.closed: await self.set_status(ConnectivityStatus.NotConnected) return if self.status == ConnectivityStatus.Connected: - content = { - "CLIENT_PING": "", - "SESSION_ID": self._api_session_id - } + content = {"CLIENT_PING": "", "SESSION_ID": self._api_session_id} content_str = json.dumps(content) data = f"{len(content_str)}\n{content_str}" @@ -154,14 +190,16 @@ async def async_send_heartbeat(self): await self._ws.send_str(data) except ConnectionResetError as crex: - _LOGGER.debug(f"Gracefully failed to send heartbeat - Restarting connection, Error: {crex}") + _LOGGER.debug( + f"Gracefully failed to send heartbeat - Restarting connection, Error: {crex}" + ) await self.set_status(ConnectivityStatus.NotConnected) except Exception as ex: _LOGGER.error(f"Failed to send heartbeat, Error: {ex}") async def _listen(self): - _LOGGER.info(f"Starting to listen connected") + _LOGGER.info("Starting to listen connected") subscription_data = self._get_subscription_data() await self._ws.send_str(subscription_data) @@ -172,10 +210,18 @@ async def _listen(self): is_connected = self.status == ConnectivityStatus.Connected is_closing_type = msg.type in WS_CLOSING_MESSAGE is_error = msg.type == aiohttp.WSMsgType.ERROR - is_closing_data = False if is_closing_type or is_error else msg.data == "close" + is_closing_data = ( + False if is_closing_type or is_error else msg.data == "close" + ) session_is_closed = self.session is None or self.session.closed - if is_closing_type or is_error or is_closing_data or session_is_closed or not is_connected: + if ( + is_closing_type + or is_error + or is_closing_data + or session_is_closed + or not is_connected + ): _LOGGER.warning( f"WS stopped listening, " f"Message: {str(msg)}, " @@ -218,10 +264,7 @@ async def parse_message(self, message): else: length = int(previous_messages[0]) - self._previous_message = { - "Length": length, - "Content": message - } + self._previous_message = {"Length": length, "Content": message} _LOGGER.debug("Store partial message for later processing") @@ -229,7 +272,9 @@ async def parse_message(self, message): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.warning(f"Parse message failed, Data: {message}, Error: {ex}, Line: {line_number}") + _LOGGER.warning( + f"Parse message failed, Data: {message}, Error: {ex}, Line: {line_number}" + ) def _get_corrected_message(self, message): original_message = message @@ -379,7 +424,9 @@ def _handle_interfaces(self, data): interface[item] = item_data elif INTERFACES_STATS == item: - interface[INTERFACE_DATA_MULTICAST] = float(item_data.get(INTERFACE_DATA_MULTICAST)) + interface[INTERFACE_DATA_MULTICAST] = float( + item_data.get(INTERFACE_DATA_MULTICAST) + ) for direction in TRAFFIC_DATA_DIRECTIONS: for key in TRAFFIC_DATA_INTERFACE_ITEMS: diff --git a/custom_components/edgeos/component/helpers/__init__.py b/custom_components/edgeos/component/helpers/__init__.py index a4dcbf9..0a2935a 100644 --- a/custom_components/edgeos/component/helpers/__init__.py +++ b/custom_components/edgeos/component/helpers/__init__.py @@ -5,8 +5,8 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from ...component.helpers.const import * from ...component.managers.home_assistant import EdgeOSHomeAssistantManager +from ...core.helpers.const import DATA _LOGGER = logging.getLogger(__name__) @@ -26,9 +26,7 @@ async def _async_unload(_: Event) -> None: await instance.async_unload() entry.async_on_unload( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_unload - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_unload) ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() @@ -38,7 +36,7 @@ async def _async_unload(_: Event) -> None: def get_ha(hass: HomeAssistant, entry_id) -> EdgeOSHomeAssistantManager: - ha_data = hass.data.get(DATA, dict()) + ha_data = hass.data.get(DATA, {}) ha = ha_data.get(entry_id) return ha @@ -46,6 +44,6 @@ def get_ha(hass: HomeAssistant, entry_id) -> EdgeOSHomeAssistantManager: def clear_ha(hass: HomeAssistant, entry_id): if DATA not in hass.data: - hass.data[DATA] = dict() + hass.data[DATA] = {} del hass.data[DATA][entry_id] diff --git a/custom_components/edgeos/component/helpers/const.py b/custom_components/edgeos/component/helpers/const.py index c558afd..1b3079f 100644 --- a/custom_components/edgeos/component/helpers/const.py +++ b/custom_components/edgeos/component/helpers/const.py @@ -8,7 +8,6 @@ import homeassistant.helpers.config_validation as cv -from ...core.helpers.const import * from .enums import InterfaceTypes ATTR_FRIENDLY_NAME = "friendly_name" @@ -29,9 +28,7 @@ STORAGE_DATA_FILE_CONFIG = "config" -STORAGE_DATA_FILES = [ - STORAGE_DATA_FILE_CONFIG -] +STORAGE_DATA_FILES = [STORAGE_DATA_FILE_CONFIG] MESSAGES_COUNTER_SECTION = "messages" @@ -106,17 +103,9 @@ WS_IGNORED_MESSAGES = "ignored-messages" WS_ERROR_MESSAGES = "error-messages" -WS_MESSAGES = [ - WS_RECEIVED_MESSAGES, - WS_IGNORED_MESSAGES, - WS_ERROR_MESSAGES -] +WS_MESSAGES = [WS_RECEIVED_MESSAGES, WS_IGNORED_MESSAGES, WS_ERROR_MESSAGES] -UPDATE_DATE_ENDPOINTS = [ - API_DATA_SYS_INFO, - API_DATA_DHCP_STATS, - API_DATA_DHCP_LEASES -] +UPDATE_DATE_ENDPOINTS = [API_DATA_SYS_INFO, API_DATA_DHCP_STATS, API_DATA_DHCP_LEASES] DISCOVER_DATA_FW_VERSION = "fwversion" DISCOVER_DATA_PRODUCT = "product" @@ -125,15 +114,11 @@ SYSTEM_STATS_DATA_CPU = "cpu" SYSTEM_STATS_DATA_MEM = "mem" -ATTR_KILO = "KBytes" -ATTR_MEGA = "MBytes" -ATTR_BYTE = "Bytes" +ATTR_BYTE = "B" +ATTR_KILO = "KB" +ATTR_MEGA = "MB" -UNIT_MAPPING = { - ATTR_BYTE: BYTE, - ATTR_KILO: KILO_BYTE, - ATTR_MEGA: MEGA_BYTE -} +UNIT_MAPPING = {ATTR_BYTE: BYTE, ATTR_KILO: KILO_BYTE, ATTR_MEGA: MEGA_BYTE} DEVICE_LIST = "devices" ADDRESS_LIST = "addresses" @@ -156,7 +141,6 @@ SERVICE_UPDATE_CONFIGURATION = "update_configuration" -EMPTY_STRING = "" BEGINS_WITH_SIX_DIGITS = "^([0-9]{1,6})" STRING_DASH = "-" @@ -238,22 +222,19 @@ TRAFFIC_DATA_DIRECTION_SENT = "tx" TRAFFIC_DATA_DIRECTION_RECEIVED = "rx" -TRAFFIC_DATA_DIRECTIONS = [ - TRAFFIC_DATA_DIRECTION_SENT, - TRAFFIC_DATA_DIRECTION_RECEIVED -] +TRAFFIC_DATA_DIRECTIONS = [TRAFFIC_DATA_DIRECTION_SENT, TRAFFIC_DATA_DIRECTION_RECEIVED] TRAFFIC_DATA_INTERFACE_ITEMS = { TRAFFIC_STATS_BPS_KEY: TRAFFIC_DATA_RATE, TRAFFIC_STATS_BYTES: TRAFFIC_DATA_TOTAL, TRAFFIC_DATA_ERRORS: TRAFFIC_DATA_ERRORS, TRAFFIC_DATA_PACKETS: TRAFFIC_DATA_PACKETS, - TRAFFIC_DATA_DROPPED: TRAFFIC_DATA_DROPPED + TRAFFIC_DATA_DROPPED: TRAFFIC_DATA_DROPPED, } TRAFFIC_DATA_DEVICE_ITEMS = { TRAFFIC_DATA_RATE: TRAFFIC_DATA_RATE, - TRAFFIC_STATS_BYTES: TRAFFIC_DATA_TOTAL + TRAFFIC_STATS_BYTES: TRAFFIC_DATA_TOTAL, } INTERFACES_MAIN_MAP = [ @@ -269,17 +250,29 @@ DISCOVER_DATA_PRODUCT, SYSTEM_STATS_DATA_UPTIME, DISCOVER_DATA_FW_VERSION, - "system_status" + "system_status", ] SERVICE_SCHEMA_UPDATE_CONFIGURATION = vol.Schema( { vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(STORAGE_DATA_CONSIDER_AWAY_INTERVAL.replace(STRING_DASH, STRING_UNDERSCORE)): vol.Range(10, 1800), - vol.Optional(STORAGE_DATA_UPDATE_ENTITIES_INTERVAL.replace(STRING_DASH, STRING_UNDERSCORE)): vol.Range(1, 60), - vol.Optional(STORAGE_DATA_UPDATE_API_INTERVAL.replace(STRING_DASH, STRING_UNDERSCORE)): vol.Range(30, 180), - vol.Optional(STORAGE_DATA_LOG_INCOMING_MESSAGES.replace(STRING_DASH, STRING_UNDERSCORE)): cv.boolean, - vol.Optional(STORAGE_DATA_UNIT.replace(STRING_DASH, STRING_UNDERSCORE)): vol.In(UNIT_MAPPING.keys()), + vol.Optional( + STORAGE_DATA_CONSIDER_AWAY_INTERVAL.replace(STRING_DASH, STRING_UNDERSCORE) + ): vol.Range(10, 1800), + vol.Optional( + STORAGE_DATA_UPDATE_ENTITIES_INTERVAL.replace( + STRING_DASH, STRING_UNDERSCORE + ) + ): vol.Range(1, 60), + vol.Optional( + STORAGE_DATA_UPDATE_API_INTERVAL.replace(STRING_DASH, STRING_UNDERSCORE) + ): vol.Range(30, 180), + vol.Optional( + STORAGE_DATA_LOG_INCOMING_MESSAGES.replace(STRING_DASH, STRING_UNDERSCORE) + ): cv.boolean, + vol.Optional(STORAGE_DATA_UNIT.replace(STRING_DASH, STRING_UNDERSCORE)): vol.In( + UNIT_MAPPING.keys() + ), } ) @@ -294,12 +287,10 @@ InterfaceTypes.SWITCH_PREFIX: "Switch", InterfaceTypes.VIRTUAL_TUNNEL_PREFIX: "Virtual Tunnel", InterfaceTypes.OPEN_VPN_PREFIX: "OpenVPN", - InterfaceTypes.BONDING_PREFIX: "VLAN" + InterfaceTypes.BONDING_PREFIX: "VLAN", } -IGNORED_INTERFACES = [ - InterfaceTypes.LOOPBACK -] +IGNORED_INTERFACES = [InterfaceTypes.LOOPBACK] RECEIVED_RATE_PREFIX = "Received Rate" RECEIVED_TRAFFIC_PREFIX = "Received Traffic" @@ -338,15 +329,9 @@ SENT_PACKETS_PREFIX: SENT_PACKETS_ICON, } -STATS_RATE = [ - RECEIVED_RATE_PREFIX, - SENT_RATE_PREFIX -] +STATS_RATE = [RECEIVED_RATE_PREFIX, SENT_RATE_PREFIX] -STATS_TRAFFIC = [ - RECEIVED_TRAFFIC_PREFIX, - SENT_TRAFFIC_PREFIX -] +STATS_TRAFFIC = [RECEIVED_TRAFFIC_PREFIX, SENT_TRAFFIC_PREFIX] STATS_UNITS = { RECEIVED_DROPPED_PREFIX: TRAFFIC_DATA_DROPPED, diff --git a/custom_components/edgeos/component/helpers/exceptions.py b/custom_components/edgeos/component/helpers/exceptions.py index f1cb608..45ee69a 100644 --- a/custom_components/edgeos/component/helpers/exceptions.py +++ b/custom_components/edgeos/component/helpers/exceptions.py @@ -15,7 +15,9 @@ class APIValidationException(Exception): status: ConnectivityStatus def __init__(self, endpoint: str, status: ConnectivityStatus): - super().__init__(f"API cannot process request to '{endpoint}', Status: {status}") + super().__init__( + f"API cannot process request to '{endpoint}', Status: {status}" + ) self.endpoint = endpoint self.status = status diff --git a/custom_components/edgeos/component/managers/home_assistant.py b/custom_components/edgeos/component/managers/home_assistant.py index 18a255c..9e35440 100644 --- a/custom_components/edgeos/component/managers/home_assistant.py +++ b/custom_components/edgeos/component/managers/home_assistant.py @@ -5,7 +5,7 @@ from asyncio import sleep from collections.abc import Awaitable, Callable -from datetime import datetime +from datetime import datetime, timedelta import logging import sys @@ -13,24 +13,134 @@ BinarySensorDeviceClass, BinarySensorEntityDescription, ) +from homeassistant.components.homeassistant import SERVICE_RELOAD_CONFIG_ENTRY from homeassistant.components.select import SelectEntityDescription from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass from homeassistant.components.switch import SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get as async_get_device_registry from homeassistant.helpers.entity import EntityCategory, EntityDescription +from ...configuration.helpers.const import DEFAULT_NAME, DOMAIN, MANUFACTURER from ...configuration.managers.configuration_manager import ConfigurationManager from ...configuration.models.config_data import ConfigData +from ...core.helpers.const import ( + ACTION_CORE_ENTITY_SELECT_OPTION, + ACTION_CORE_ENTITY_TURN_OFF, + ACTION_CORE_ENTITY_TURN_ON, + DOMAIN_BINARY_SENSOR, + DOMAIN_DEVICE_TRACKER, + DOMAIN_SELECT, + DOMAIN_SENSOR, + DOMAIN_SWITCH, + ENTITY_CONFIG_ENTRY_ID, + ENTITY_UNIQUE_ID, + HA_NAME, +) from ...core.helpers.enums import ConnectivityStatus from ...core.managers.home_assistant import HomeAssistantManager from ...core.models.entity_data import EntityData from ..api.api import IntegrationAPI from ..api.storage_api import StorageAPI from ..api.websocket import IntegrationWS -from ..helpers.const import * +from ..helpers.const import ( + ADDRESS_LIST, + API_DATA_DHCP_LEASES, + API_DATA_DHCP_STATS, + API_DATA_INTERFACES, + API_DATA_SYS_INFO, + API_DATA_SYSTEM, + BYTE, + CONF_DEVICE_ID, + DATA_SYSTEM_SERVICE, + DATA_SYSTEM_SERVICE_DHCP_SERVER, + DEFAULT_HEARTBEAT_INTERVAL, + DEFAULT_UPDATE_API_INTERVAL, + DEVICE_DATA_MAC, + DEVICE_LIST, + DHCP_SERVER_IP_ADDRESS, + DHCP_SERVER_LEASED, + DHCP_SERVER_LEASES, + DHCP_SERVER_LEASES_CLIENT_HOSTNAME, + DHCP_SERVER_MAC_ADDRESS, + DHCP_SERVER_SHARED_NETWORK_NAME, + DHCP_SERVER_STATIC_MAPPING, + DHCP_SERVER_STATS, + DHCP_SERVER_SUBNET, + DISCOVER_DATA_FW_VERSION, + DISCOVER_DATA_PRODUCT, + FALSE_STR, + FW_LATEST_STATE_CAN_UPGRADE, + INTERFACE_DATA_ADDRESS, + INTERFACE_DATA_AGING, + INTERFACE_DATA_BRIDGE_GROUP, + INTERFACE_DATA_BRIDGED_CONNTRACK, + INTERFACE_DATA_DESCRIPTION, + INTERFACE_DATA_DUPLEX, + INTERFACE_DATA_HELLO_TIME, + INTERFACE_DATA_LINK_UP, + INTERFACE_DATA_MAC, + INTERFACE_DATA_MAX_AGE, + INTERFACE_DATA_MULTICAST, + INTERFACE_DATA_PRIORITY, + INTERFACE_DATA_PROMISCUOUS, + INTERFACE_DATA_SPEED, + INTERFACE_DATA_STP, + INTERFACE_DATA_UP, + LAST_ACTIVITY, + MESSAGES_COUNTER_SECTION, + SERVICE_SCHEMA_UPDATE_CONFIGURATION, + SERVICE_UPDATE_CONFIGURATION, + STATS_ICONS, + STATS_RATE, + STATS_TRAFFIC, + STATS_UNITS, + STORAGE_DATA_CONSIDER_AWAY_INTERVAL, + STORAGE_DATA_LOG_INCOMING_MESSAGES, + STORAGE_DATA_UNIT, + STORAGE_DATA_UPDATE_API_INTERVAL, + STORAGE_DATA_UPDATE_ENTITIES_INTERVAL, + STRING_DASH, + STRING_UNDERSCORE, + SYSTEM_DATA_DOMAIN_NAME, + SYSTEM_DATA_HOSTNAME, + SYSTEM_DATA_LOGIN, + SYSTEM_DATA_LOGIN_USER, + SYSTEM_DATA_LOGIN_USER_LEVEL, + SYSTEM_DATA_NTP, + SYSTEM_DATA_NTP_SERVER, + SYSTEM_DATA_OFFLOAD, + SYSTEM_DATA_OFFLOAD_HW_NAT, + SYSTEM_DATA_OFFLOAD_IPSEC, + SYSTEM_DATA_TIME_ZONE, + SYSTEM_DATA_TRAFFIC_ANALYSIS, + SYSTEM_DATA_TRAFFIC_ANALYSIS_DPI, + SYSTEM_DATA_TRAFFIC_ANALYSIS_EXPORT, + SYSTEM_INFO_DATA_FW_LATEST, + SYSTEM_INFO_DATA_FW_LATEST_STATE, + SYSTEM_INFO_DATA_FW_LATEST_URL, + SYSTEM_INFO_DATA_FW_LATEST_VERSION, + SYSTEM_INFO_DATA_SW_VER, + SYSTEM_STATS_DATA_CPU, + SYSTEM_STATS_DATA_MEM, + SYSTEM_STATS_DATA_UPTIME, + TRAFFIC_DATA_DEVICE_ITEMS, + TRAFFIC_DATA_DROPPED, + TRAFFIC_DATA_ERRORS, + TRAFFIC_DATA_INTERFACE_ITEMS, + TRAFFIC_DATA_PACKETS, + TRUE_STR, + UNIT_MAPPING, + USER_LEVEL_ADMIN, + WS_DISCOVER_KEY, + WS_EXPORT_KEY, + WS_INTERFACES_KEY, + WS_MESSAGES, + WS_RECONNECT_INTERVAL, + WS_SYSTEM_STATS_KEY, +) from ..helpers.enums import InterfaceHandlers from ..models.edge_os_device_data import EdgeOSDeviceData from ..models.edge_os_interface_data import EdgeOSInterfaceData @@ -44,8 +154,12 @@ def __init__(self, hass: HomeAssistant): super().__init__(hass, DEFAULT_UPDATE_API_INTERVAL, DEFAULT_HEARTBEAT_INTERVAL) self._storage_api: StorageAPI = StorageAPI(self._hass) - self._api: IntegrationAPI = IntegrationAPI(self._hass, self._api_data_changed, self._api_status_changed) - self._ws: IntegrationWS = IntegrationWS(self._hass, self._ws_data_changed, self._ws_status_changed) + self._api: IntegrationAPI = IntegrationAPI( + self._hass, self._api_data_changed, self._api_status_changed + ) + self._ws: IntegrationWS = IntegrationWS( + self._hass, self._ws_data_changed, self._ws_status_changed + ) self._config_manager: ConfigurationManager | None = None self._system: EdgeOSSystemData | None = None self._devices: dict[str, EdgeOSDeviceData] = {} @@ -84,7 +198,7 @@ def system_name(self): return name async def async_send_heartbeat(self): - """ Must be implemented to be able to send heartbeat to API """ + """Must be implemented to be able to send heartbeat to API""" await self.ws.async_send_heartbeat() async def _api_data_changed(self): @@ -96,7 +210,9 @@ async def _ws_data_changed(self): await self._extract_ws_data() async def _api_status_changed(self, status: ConnectivityStatus): - _LOGGER.info(f"API Status changed to {status.name}, WS Status: {self.ws.status.name}") + _LOGGER.info( + f"API Status changed to {status.name}, WS Status: {self.ws.status.name}" + ) if status == ConnectivityStatus.Connected: await self.api.async_update() @@ -111,11 +227,16 @@ async def _api_status_changed(self, status: ConnectivityStatus): await self.ws.terminate() async def _ws_status_changed(self, status: ConnectivityStatus): - _LOGGER.info(f"WS Status changed to {status.name}, API Status: {self.api.status.name}") + _LOGGER.info( + f"WS Status changed to {status.name}, API Status: {self.api.status.name}" + ) api_connected = self.api.status == ConnectivityStatus.Connected ws_connected = status == ConnectivityStatus.Connected - ws_reconnect = status in [ConnectivityStatus.NotConnected, ConnectivityStatus.Failed] + ws_reconnect = status in [ + ConnectivityStatus.NotConnected, + ConnectivityStatus.Failed, + ] self._can_load_components = ws_connected @@ -131,17 +252,25 @@ async def async_component_initialize(self, entry: ConfigEntry): await self.storage_api.initialize(self.config_data) - update_entities_interval = timedelta(seconds=self.storage_api.update_entities_interval) - update_api_interval = timedelta(seconds=self.storage_api.update_api_interval) + update_entities_interval = timedelta( + seconds=self.storage_api.update_entities_interval + ) + update_api_interval = timedelta( + seconds=self.storage_api.update_api_interval + ) - _LOGGER.info(f"Setting intervals, API: {update_api_interval}, Entities: {update_entities_interval}") + _LOGGER.info( + f"Setting intervals, API: {update_api_interval}, Entities: {update_entities_interval}" + ) self.update_intervals(update_entities_interval, update_api_interval) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to async_component_initialize, error: {ex}, line: {line_number}") + _LOGGER.error( + f"Failed to async_component_initialize, error: {ex}, line: {line_number}" + ) async def async_initialize_data_providers(self): await self.storage_api.initialize(self.config_data) @@ -157,7 +286,9 @@ async def async_initialize_data_providers(self): migration_data[option_key] = entry_options.get(option_key) if self._entry.data is not None: - migration_data[STORAGE_DATA_UNIT] = self._entry.data.get(STORAGE_DATA_UNIT) + migration_data[STORAGE_DATA_UNIT] = self._entry.data.get( + STORAGE_DATA_UNIT + ) updated = await self._update_configuration_data(migration_data) @@ -172,9 +303,9 @@ async def async_initialize_data_providers(self): options = {} - self._hass.config_entries.async_update_entry(self._entry, - data=data, - options=options) + self._hass.config_entries.async_update_entry( + self._entry, data=data, options=options + ) _LOGGER.info("Configuration migration completed, reloading integration") @@ -194,13 +325,17 @@ async def async_update_data_providers(self): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to async_update_data_providers, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to async_update_data_providers, Error: {ex}, Line: {line_number}" + ) def register_services(self, entry: ConfigEntry | None = None): - self._hass.services.async_register(DOMAIN, - SERVICE_UPDATE_CONFIGURATION, - self._update_configuration, - SERVICE_SCHEMA_UPDATE_CONFIGURATION) + self._hass.services.async_register( + DOMAIN, + SERVICE_UPDATE_CONFIGURATION, + self._update_configuration, + SERVICE_SCHEMA_UPDATE_CONFIGURATION, + ) def load_devices(self): if not self._can_load_components: @@ -246,7 +381,9 @@ def load_entities(self): for stats_data_key in stats_data: stats_data_item = stats_data.get(stats_data_key) - self._load_device_stats_sensor(device_item, stats_data_key, stats_data_item) + self._load_device_stats_sensor( + device_item, stats_data_key, stats_data_item + ) for unique_id in self._interfaces: interface_item = self._interfaces.get(unique_id) @@ -268,7 +405,9 @@ def load_entities(self): for stats_data_key in stats_data: stats_data_item = stats_data.get(stats_data_key) - self._load_interface_stats_sensor(interface_item, stats_data_key, stats_data_item) + self._load_interface_stats_sensor( + interface_item, stats_data_key, stats_data_item + ) def get_device_name(self, device: EdgeOSDeviceData): return f"{self.system_name} Device {device.hostname}" @@ -307,7 +446,9 @@ async def _extract_ws_data(self): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract WS data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract WS data, Error: {ex}, Line: {line_number}" + ) async def _extract_api_data(self): try: @@ -334,12 +475,16 @@ async def _extract_api_data(self): if len(warning_messages) > 0: warning_message = " and ".join(warning_messages) - _LOGGER.warning(f"Integration will not work correctly since {warning_message}") + _LOGGER.warning( + f"Integration will not work correctly since {warning_message}" + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract API data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract API data, Error: {ex}, Line: {line_number}" + ) def get_debug_data(self) -> dict: messages = {} @@ -354,7 +499,7 @@ def get_debug_data(self) -> dict: API_DATA_SYSTEM: self._system, DEVICE_LIST: self._devices, API_DATA_INTERFACES: self._interfaces, - MESSAGES_COUNTER_SECTION: messages + MESSAGES_COUNTER_SECTION: messages, } return data @@ -372,16 +517,25 @@ def _extract_system(self, data: dict, system_info: dict): system_data.ntp_servers = ntp.get(SYSTEM_DATA_NTP_SERVER) offload: dict = system_details.get(SYSTEM_DATA_OFFLOAD, {}) - hardware_offload = EdgeOSSystemData.is_enabled(offload, SYSTEM_DATA_OFFLOAD_HW_NAT) - ipsec_offload = EdgeOSSystemData.is_enabled(offload, SYSTEM_DATA_OFFLOAD_IPSEC) + hardware_offload = EdgeOSSystemData.is_enabled( + offload, SYSTEM_DATA_OFFLOAD_HW_NAT + ) + ipsec_offload = EdgeOSSystemData.is_enabled( + offload, SYSTEM_DATA_OFFLOAD_IPSEC + ) system_data.hardware_offload = hardware_offload system_data.ipsec_offload = ipsec_offload - traffic_analysis: dict = system_details.get(SYSTEM_DATA_TRAFFIC_ANALYSIS, {}) - dpi = EdgeOSSystemData.is_enabled(traffic_analysis, SYSTEM_DATA_TRAFFIC_ANALYSIS_DPI) - traffic_analysis_export = EdgeOSSystemData.is_enabled(traffic_analysis, - SYSTEM_DATA_TRAFFIC_ANALYSIS_EXPORT) + traffic_analysis: dict = system_details.get( + SYSTEM_DATA_TRAFFIC_ANALYSIS, {} + ) + dpi = EdgeOSSystemData.is_enabled( + traffic_analysis, SYSTEM_DATA_TRAFFIC_ANALYSIS_DPI + ) + traffic_analysis_export = EdgeOSSystemData.is_enabled( + traffic_analysis, SYSTEM_DATA_TRAFFIC_ANALYSIS_EXPORT + ) system_data.deep_packet_inspection = dpi system_data.traffic_analysis_export = traffic_analysis_export @@ -393,7 +547,9 @@ def _extract_system(self, data: dict, system_info: dict): fw_latest_version = fw_latest.get(SYSTEM_INFO_DATA_FW_LATEST_VERSION) fw_latest_url = fw_latest.get(SYSTEM_INFO_DATA_FW_LATEST_URL) - system_data.upgrade_available = fw_latest_state == FW_LATEST_STATE_CAN_UPGRADE + system_data.upgrade_available = ( + fw_latest_state == FW_LATEST_STATE_CAN_UPGRADE + ) system_data.upgrade_url = fw_latest_url system_data.upgrade_version = fw_latest_version @@ -417,7 +573,9 @@ def _extract_system(self, data: dict, system_info: dict): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract System data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract System data, Error: {ex}, Line: {line_number}" + ) def _extract_interfaces(self, data: dict): try: @@ -428,15 +586,21 @@ def _extract_interfaces(self, data: dict): for interface_name in interfaces: interface_data = interfaces.get(interface_name, {}) - self._extract_interface(interface_name, interface_data, interface_type) + self._extract_interface( + interface_name, interface_data, interface_type + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract Interfaces data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract Interfaces data, Error: {ex}, Line: {line_number}" + ) - def _extract_interface(self, name: str, data: dict, interface_type: str | None = None) -> EdgeOSInterfaceData: + def _extract_interface( + self, name: str, data: dict, interface_type: str | None = None + ) -> EdgeOSInterfaceData: interface = self._interfaces.get(name) try: @@ -460,7 +624,9 @@ def _extract_interface(self, name: str, data: dict, interface_type: str | None = interface.max_age = data.get(INTERFACE_DATA_MAX_AGE) interface.priority = data.get(INTERFACE_DATA_PRIORITY) interface.promiscuous = data.get(INTERFACE_DATA_PROMISCUOUS) - interface.stp = data.get(INTERFACE_DATA_STP, FALSE_STR).lower() == TRUE_STR + interface.stp = ( + data.get(INTERFACE_DATA_STP, FALSE_STR).lower() == TRUE_STR + ) self._interfaces[interface.unique_id] = interface @@ -480,8 +646,12 @@ def _extract_interface(self, name: str, data: dict, interface_type: str | None = def _update_interface_stats(interface: EdgeOSInterfaceData, data: dict): try: if data is not None: - interface.up = str(data.get(INTERFACE_DATA_UP, False)).lower() == TRUE_STR - interface.l1up = str(data.get(INTERFACE_DATA_LINK_UP, False)).lower() == TRUE_STR + interface.up = ( + str(data.get(INTERFACE_DATA_UP, False)).lower() == TRUE_STR + ) + interface.l1up = ( + str(data.get(INTERFACE_DATA_LINK_UP, False)).lower() == TRUE_STR + ) interface.mac = data.get(INTERFACE_DATA_MAC) interface.multicast = data.get(INTERFACE_DATA_MULTICAST, 0) interface.address = data.get(ADDRESS_LIST, []) @@ -586,7 +756,7 @@ def _extract_unknown_devices(self): static_mapping_data = { DHCP_SERVER_IP_ADDRESS: ip, - DHCP_SERVER_MAC_ADDRESS: device_data.get(DEVICE_DATA_MAC) + DHCP_SERVER_MAC_ADDRESS: device_data.get(DEVICE_DATA_MAC), } self._set_device(hostname, None, static_mapping_data, True) @@ -594,7 +764,9 @@ def _extract_unknown_devices(self): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract Unknown Devices data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract Unknown Devices data, Error: {ex}, Line: {line_number}" + ) def _extract_devices(self, data: dict): try: @@ -603,7 +775,9 @@ def _extract_devices(self, data: dict): shared_network_names = dhcp_server.get(DHCP_SERVER_SHARED_NETWORK_NAME, {}) for shared_network_name in shared_network_names: - shared_network_name_data = shared_network_names.get(shared_network_name, {}) + shared_network_name_data = shared_network_names.get( + shared_network_name, {} + ) subnets = shared_network_name_data.get(DHCP_SERVER_SUBNET, {}) for subnet in subnets: @@ -615,21 +789,33 @@ def _extract_devices(self, data: dict): for hostname in static_mappings: static_mapping_data = static_mappings.get(hostname, {}) - self._set_device(hostname, domain_name, static_mapping_data, False) + self._set_device( + hostname, domain_name, static_mapping_data, False + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to extract Devices data, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to extract Devices data, Error: {ex}, Line: {line_number}" + ) - def _set_device(self, hostname: str, domain_name: str | None, static_mapping_data: dict, is_leased: bool): + def _set_device( + self, + hostname: str, + domain_name: str | None, + static_mapping_data: dict, + is_leased: bool, + ): ip_address = static_mapping_data.get(DHCP_SERVER_IP_ADDRESS) mac_address = static_mapping_data.get(DHCP_SERVER_MAC_ADDRESS) existing_device_data = self._devices.get(mac_address) if existing_device_data is None: - device_data = EdgeOSDeviceData(hostname, ip_address, mac_address, domain_name, is_leased) + device_data = EdgeOSDeviceData( + hostname, ip_address, mac_address, domain_name, is_leased + ) else: device_data = existing_device_data @@ -649,14 +835,16 @@ def _get_device_by_ip(self, ip: str) -> EdgeOSDeviceData | None: return device - def _set_ha_device(self, name: str, model: str, manufacturer: str, version: str | None = None): + def _set_ha_device( + self, name: str, model: str, manufacturer: str, version: str | None = None + ): device_details = self.device_manager.get(name) device_details_data = { "identifiers": {(DEFAULT_NAME, name)}, "name": name, "manufacturer": manufacturer, - "model": model + "model": model, } if version is not None: @@ -668,7 +856,12 @@ def _set_ha_device(self, name: str, model: str, manufacturer: str, version: str _LOGGER.debug(f"Created HA device {name} [{model}]") def _load_main_device(self): - self._set_ha_device(self.system_name, self._system.product, MANUFACTURER, self._system.fw_version) + self._set_ha_device( + self.system_name, + self._system.product, + MANUFACTURER, + self._system.fw_version, + ) def _load_device_device(self, device: EdgeOSDeviceData): name = self.get_device_name(device) @@ -694,21 +887,23 @@ def _load_unit_select(self): key=unique_id, name=entity_name, device_class=f"{DOMAIN}__{STORAGE_DATA_UNIT}", - options=list([unit.replace(ATTR_BYTE[1:], "").upper() for unit in UNIT_MAPPING.keys()]), - entity_category=EntityCategory.CONFIG + options=list(UNIT_MAPPING.keys()), + entity_category=EntityCategory.CONFIG, ) self.set_action(unique_id, ACTION_CORE_ENTITY_SELECT_OPTION, self._set_unit) - self.entity_manager.set_entity(DOMAIN_SELECT, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_SELECT, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) except Exception as ex: - self.log_exception(ex, f"Failed to load select for Data Unit") + self.log_exception(ex, "Failed to load select for Data Unit") def _load_unknown_devices_sensor(self): device_name = self.system_name @@ -727,7 +922,7 @@ def _load_unknown_devices_sensor(self): attributes = { ATTR_FRIENDLY_NAME: entity_name, - DHCP_SERVER_LEASED: leased_devices + DHCP_SERVER_LEASED: leased_devices, } unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) @@ -737,20 +932,20 @@ def _load_unknown_devices_sensor(self): key=unique_id, name=entity_name, icon=icon, - state_class=SensorStateClass.MEASUREMENT + state_class=SensorStateClass.MEASUREMENT, ) - self.entity_manager.set_entity(DOMAIN_SENSOR, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_SENSOR, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load sensor for {entity_name}") def _load_cpu_sensor(self): device_name = self.system_name @@ -774,17 +969,17 @@ def _load_cpu_sensor(self): native_unit_of_measurement="%", ) - self.entity_manager.set_entity(DOMAIN_SENSOR, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_SENSOR, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load sensor for {entity_name}") def _load_ram_sensor(self): device_name = self.system_name @@ -793,9 +988,7 @@ def _load_ram_sensor(self): try: state = self._system.mem - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } + attributes = {ATTR_FRIENDLY_NAME: entity_name} unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) icon = "mdi:memory" @@ -808,17 +1001,17 @@ def _load_ram_sensor(self): native_unit_of_measurement="%", ) - self.entity_manager.set_entity(DOMAIN_SENSOR, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_SENSOR, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load sensor for {entity_name}") def _load_uptime_sensor(self): device_name = self.system_name @@ -827,9 +1020,7 @@ def _load_uptime_sensor(self): try: state = self._system.uptime - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } + attributes = {ATTR_FRIENDLY_NAME: entity_name} unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) icon = "mdi:credit-card-clock" @@ -838,20 +1029,20 @@ def _load_uptime_sensor(self): key=unique_id, name=entity_name, icon=icon, - state_class=SensorStateClass.TOTAL_INCREASING + state_class=SensorStateClass.TOTAL_INCREASING, ) - self.entity_manager.set_entity(DOMAIN_SENSOR, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_SENSOR, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load sensor for {entity_name}") def _load_firmware_upgrade_binary_sensor(self): device_name = self.system_name @@ -863,7 +1054,7 @@ def _load_firmware_upgrade_binary_sensor(self): attributes = { ATTR_FRIENDLY_NAME: entity_name, SYSTEM_INFO_DATA_FW_LATEST_URL: self._system.upgrade_url, - SYSTEM_INFO_DATA_FW_LATEST_VERSION: self._system.upgrade_version + SYSTEM_INFO_DATA_FW_LATEST_VERSION: self._system.upgrade_version, } unique_id = EntityData.generate_unique_id(DOMAIN_BINARY_SENSOR, entity_name) @@ -871,20 +1062,20 @@ def _load_firmware_upgrade_binary_sensor(self): entity_description = BinarySensorEntityDescription( key=unique_id, name=entity_name, - device_class=BinarySensorDeviceClass.UPDATE + device_class=BinarySensorDeviceClass.UPDATE, ) - self.entity_manager.set_entity(DOMAIN_BINARY_SENSOR, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_BINARY_SENSOR, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load sensor for {entity_name}") def _load_log_incoming_messages_switch(self): device_name = self.system_name @@ -893,9 +1084,7 @@ def _load_log_incoming_messages_switch(self): try: state = self.storage_api.log_incoming_messages - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } + attributes = {ATTR_FRIENDLY_NAME: entity_name} unique_id = EntityData.generate_unique_id(DOMAIN_SWITCH, entity_name) @@ -905,18 +1094,28 @@ def _load_log_incoming_messages_switch(self): key=unique_id, name=entity_name, icon=icon, - entity_category=EntityCategory.CONFIG + entity_category=EntityCategory.CONFIG, ) - self.entity_manager.set_entity(DOMAIN_SWITCH, - self.entry_id, - state, - attributes, - device_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_SWITCH, + self.entry_id, + state, + attributes, + device_name, + entity_description, + ) - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_ON, self._enable_log_incoming_messages) - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._disable_log_incoming_messages) + self.set_action( + unique_id, + ACTION_CORE_ENTITY_TURN_ON, + self._enable_log_incoming_messages, + ) + self.set_action( + unique_id, + ACTION_CORE_ENTITY_TURN_OFF, + self._disable_log_incoming_messages, + ) except Exception as ex: self.log_exception( @@ -928,40 +1127,42 @@ def _load_device_tracker(self, device: EdgeOSDeviceData): entity_name = f"{device_name}" try: - state = device.last_activity_in_seconds <= self.storage_api.consider_away_interval + state = ( + device.last_activity_in_seconds + <= self.storage_api.consider_away_interval + ) attributes = { ATTR_FRIENDLY_NAME: entity_name, - LAST_ACTIVITY: device.last_activity_in_seconds + LAST_ACTIVITY: device.last_activity_in_seconds, } - unique_id = EntityData.generate_unique_id(DOMAIN_DEVICE_TRACKER, entity_name) - - entity_description = EntityDescription( - key=unique_id, - name=entity_name + unique_id = EntityData.generate_unique_id( + DOMAIN_DEVICE_TRACKER, entity_name ) - details = { - ENTITY_UNIQUE_ID: device.unique_id - } + entity_description = EntityDescription(key=unique_id, name=entity_name) - is_monitored = self.storage_api.monitored_devices.get(device.unique_id, False) + details = {ENTITY_UNIQUE_ID: device.unique_id} - self.entity_manager.set_entity(DOMAIN_DEVICE_TRACKER, - self.entry_id, - state, - attributes, - device_name, - entity_description, - destructors=[not is_monitored], - details=details) + is_monitored = self.storage_api.monitored_devices.get( + device.unique_id, False + ) - except Exception as ex: - self.log_exception( - ex, f"Failed to load device tracker for {entity_name}" + self.entity_manager.set_entity( + DOMAIN_DEVICE_TRACKER, + self.entry_id, + state, + attributes, + device_name, + entity_description, + destructors=[not is_monitored], + details=details, ) + except Exception as ex: + self.log_exception(ex, f"Failed to load device tracker for {entity_name}") + def _load_device_monitor_switch(self, device: EdgeOSDeviceData): device_name = self.get_device_name(device) entity_name = f"{device_name} Monitored" @@ -969,9 +1170,7 @@ def _load_device_monitor_switch(self, device: EdgeOSDeviceData): try: state = self.storage_api.monitored_devices.get(device.unique_id, False) - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } + attributes = {ATTR_FRIENDLY_NAME: entity_name} unique_id = EntityData.generate_unique_id(DOMAIN_SWITCH, entity_name) icon = "mdi:monitor-eye" @@ -980,34 +1179,37 @@ def _load_device_monitor_switch(self, device: EdgeOSDeviceData): key=unique_id, name=entity_name, icon=icon, - entity_category=EntityCategory.CONFIG + entity_category=EntityCategory.CONFIG, ) - details = { - ENTITY_UNIQUE_ID: device.unique_id - } - - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_ON, self._set_device_monitored) - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._set_device_unmonitored) + details = {ENTITY_UNIQUE_ID: device.unique_id} - self.entity_manager.set_entity(DOMAIN_SWITCH, - self.entry_id, - state, - attributes, - device_name, - entity_description, - details=details) - - except Exception as ex: - self.log_exception( - ex, f"Failed to load switch for {entity_name}" + self.set_action( + unique_id, ACTION_CORE_ENTITY_TURN_ON, self._set_device_monitored + ) + self.set_action( + unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._set_device_unmonitored ) - def _load_device_stats_sensor(self, - device: EdgeOSDeviceData, - entity_suffix: str, - state: str | int | float | None): + self.entity_manager.set_entity( + DOMAIN_SWITCH, + self.entry_id, + state, + attributes, + device_name, + entity_description, + details=details, + ) + except Exception as ex: + self.log_exception(ex, f"Failed to load switch for {entity_name}") + + def _load_device_stats_sensor( + self, + device: EdgeOSDeviceData, + entity_suffix: str, + state: str | int | float | None, + ): device_name = self.get_device_name(device) entity_name = f"{device_name} {entity_suffix}" @@ -1027,19 +1229,34 @@ def _load_device_stats_sensor(self, else: unit_of_measurement = str(STATS_UNITS.get(entity_suffix)).capitalize() - state_class = SensorStateClass.MEASUREMENT if is_rate_stats else SensorStateClass.TOTAL_INCREASING - - self._load_stats_sensor(device_name, entity_name, state, unit_of_measurement, icon, state_class, is_monitored) - - def _load_interface_stats_sensor(self, - interface: EdgeOSInterfaceData, - entity_suffix: str, - state: str | int | float | None): - + state_class = ( + SensorStateClass.MEASUREMENT + if is_rate_stats + else SensorStateClass.TOTAL_INCREASING + ) + + self._load_stats_sensor( + device_name, + entity_name, + state, + unit_of_measurement, + icon, + state_class, + is_monitored, + ) + + def _load_interface_stats_sensor( + self, + interface: EdgeOSInterfaceData, + entity_suffix: str, + state: str | int | float | None, + ): device_name = self.get_interface_name(interface) entity_name = f"{device_name} {entity_suffix}" - is_monitored = self.storage_api.monitored_interfaces.get(interface.unique_id, False) + is_monitored = self.storage_api.monitored_interfaces.get( + interface.unique_id, False + ) is_rate_stats = entity_suffix in STATS_RATE icon = STATS_ICONS.get(entity_suffix) @@ -1055,22 +1272,34 @@ def _load_interface_stats_sensor(self, else: unit_of_measurement = str(STATS_UNITS.get(entity_suffix)).capitalize() - state_class = SensorStateClass.MEASUREMENT if is_rate_stats else SensorStateClass.TOTAL_INCREASING - - self._load_stats_sensor(device_name, entity_name, state, unit_of_measurement, icon, state_class, is_monitored) - - def _load_stats_sensor(self, - device_name: str, - entity_name: str, - state: str | int | float | None, - unit_of_measurement: str, - icon: str | None, - state_class: SensorStateClass, - is_monitored: bool): + state_class = ( + SensorStateClass.MEASUREMENT + if is_rate_stats + else SensorStateClass.TOTAL_INCREASING + ) + + self._load_stats_sensor( + device_name, + entity_name, + state, + unit_of_measurement, + icon, + state_class, + is_monitored, + ) + + def _load_stats_sensor( + self, + device_name: str, + entity_name: str, + state: str | int | float | None, + unit_of_measurement: str, + icon: str | None, + state_class: SensorStateClass, + is_monitored: bool, + ): try: - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } + attributes = {ATTR_FRIENDLY_NAME: entity_name} unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) @@ -1082,21 +1311,25 @@ def _load_stats_sensor(self, native_unit_of_measurement=unit_of_measurement, ) - if unit_of_measurement.lower() in [TRAFFIC_DATA_ERRORS, TRAFFIC_DATA_PACKETS, TRAFFIC_DATA_DROPPED]: + if unit_of_measurement.lower() in [ + TRAFFIC_DATA_ERRORS, + TRAFFIC_DATA_PACKETS, + TRAFFIC_DATA_DROPPED, + ]: state = self._format_number(state) - self.entity_manager.set_entity(DOMAIN_SENSOR, - self.entry_id, - state, - attributes, - device_name, - entity_description, - destructors=[not is_monitored]) + self.entity_manager.set_entity( + DOMAIN_SENSOR, + self.entry_id, + state, + attributes, + device_name, + entity_description, + destructors=[not is_monitored], + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load sensor for {entity_name}") def _load_interface_status_switch(self, interface: EdgeOSInterfaceData): interface_name = self.get_interface_name(interface) @@ -1107,7 +1340,7 @@ def _load_interface_status_switch(self, interface: EdgeOSInterfaceData): attributes = { ATTR_FRIENDLY_NAME: entity_name, - ADDRESS_LIST: interface.address + ADDRESS_LIST: interface.address, } unique_id = EntityData.generate_unique_id(DOMAIN_SWITCH, entity_name) @@ -1117,28 +1350,30 @@ def _load_interface_status_switch(self, interface: EdgeOSInterfaceData): key=unique_id, name=entity_name, icon=icon, - entity_category=EntityCategory.CONFIG + entity_category=EntityCategory.CONFIG, ) - details = { - ENTITY_UNIQUE_ID: interface.unique_id - } + details = {ENTITY_UNIQUE_ID: interface.unique_id} - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_ON, self._set_interface_enabled) - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._set_interface_disabled) + self.set_action( + unique_id, ACTION_CORE_ENTITY_TURN_ON, self._set_interface_enabled + ) + self.set_action( + unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._set_interface_disabled + ) - self.entity_manager.set_entity(DOMAIN_SWITCH, - self.entry_id, - state, - attributes, - interface_name, - entity_description, - details=details) + self.entity_manager.set_entity( + DOMAIN_SWITCH, + self.entry_id, + state, + attributes, + interface_name, + entity_description, + details=details, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load switch for {entity_name}" - ) + self.log_exception(ex, f"Failed to load switch for {entity_name}") def _load_interface_status_binary_sensor(self, interface: EdgeOSInterfaceData): interface_name = self.get_interface_name(interface) @@ -1149,7 +1384,7 @@ def _load_interface_status_binary_sensor(self, interface: EdgeOSInterfaceData): attributes = { ATTR_FRIENDLY_NAME: entity_name, - ADDRESS_LIST: interface.address + ADDRESS_LIST: interface.address, } unique_id = EntityData.generate_unique_id(DOMAIN_BINARY_SENSOR, entity_name) @@ -1157,20 +1392,20 @@ def _load_interface_status_binary_sensor(self, interface: EdgeOSInterfaceData): entity_description = BinarySensorEntityDescription( key=unique_id, name=entity_name, - device_class=BinarySensorDeviceClass.CONNECTIVITY + device_class=BinarySensorDeviceClass.CONNECTIVITY, ) - self.entity_manager.set_entity(DOMAIN_BINARY_SENSOR, - self.entry_id, - state, - attributes, - interface_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_BINARY_SENSOR, + self.entry_id, + state, + attributes, + interface_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load binary sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load binary sensor for {entity_name}") def _load_interface_connected_binary_sensor(self, interface: EdgeOSInterfaceData): interface_name = self.get_interface_name(interface) @@ -1181,7 +1416,7 @@ def _load_interface_connected_binary_sensor(self, interface: EdgeOSInterfaceData attributes = { ATTR_FRIENDLY_NAME: entity_name, - ADDRESS_LIST: interface.address + ADDRESS_LIST: interface.address, } unique_id = EntityData.generate_unique_id(DOMAIN_BINARY_SENSOR, entity_name) @@ -1189,31 +1424,31 @@ def _load_interface_connected_binary_sensor(self, interface: EdgeOSInterfaceData entity_description = BinarySensorEntityDescription( key=unique_id, name=entity_name, - device_class=BinarySensorDeviceClass.CONNECTIVITY + device_class=BinarySensorDeviceClass.CONNECTIVITY, ) - self.entity_manager.set_entity(DOMAIN_BINARY_SENSOR, - self.entry_id, - state, - attributes, - interface_name, - entity_description) + self.entity_manager.set_entity( + DOMAIN_BINARY_SENSOR, + self.entry_id, + state, + attributes, + interface_name, + entity_description, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load binary sensor for {entity_name}" - ) + self.log_exception(ex, f"Failed to load binary sensor for {entity_name}") def _load_interface_monitor_switch(self, interface: EdgeOSInterfaceData): interface_name = self.get_interface_name(interface) entity_name = f"{interface_name} Monitored" try: - state = self.storage_api.monitored_interfaces.get(interface.unique_id, False) + state = self.storage_api.monitored_interfaces.get( + interface.unique_id, False + ) - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } + attributes = {ATTR_FRIENDLY_NAME: entity_name} unique_id = EntityData.generate_unique_id(DOMAIN_SWITCH, entity_name) icon = None @@ -1222,28 +1457,30 @@ def _load_interface_monitor_switch(self, interface: EdgeOSInterfaceData): key=unique_id, name=entity_name, icon=icon, - entity_category=EntityCategory.CONFIG + entity_category=EntityCategory.CONFIG, ) - details = { - ENTITY_UNIQUE_ID: interface.unique_id - } + details = {ENTITY_UNIQUE_ID: interface.unique_id} - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_ON, self._set_interface_monitored) - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._set_interface_unmonitored) + self.set_action( + unique_id, ACTION_CORE_ENTITY_TURN_ON, self._set_interface_monitored + ) + self.set_action( + unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._set_interface_unmonitored + ) - self.entity_manager.set_entity(DOMAIN_SWITCH, - self.entry_id, - state, - attributes, - interface_name, - entity_description, - details=details) + self.entity_manager.set_entity( + DOMAIN_SWITCH, + self.entry_id, + state, + attributes, + interface_name, + entity_description, + details=details, + ) except Exception as ex: - self.log_exception( - ex, f"Failed to load switch for {entity_name}" - ) + self.log_exception(ex, f"Failed to load switch for {entity_name}") async def _set_interface_enabled(self, entity: EntityData): interface_item = self._get_interface_from_entity(entity) @@ -1333,11 +1570,9 @@ def _get_rate_unit_of_measurement(self) -> str: return result async def _reload_integration(self): - data = { - ENTITY_CONFIG_ENTRY_ID: self.entry_id - } + data = {ENTITY_CONFIG_ENTRY_ID: self.entry_id} - await self._hass.services.async_call(HA_NAME, SERVICE_RELOAD, data) + await self._hass.services.async_call(HA_NAME, SERVICE_RELOAD_CONFIG_ENTRY, data) def _update_configuration(self, service_call): self._hass.async_create_task(self._async_update_configuration(service_call)) @@ -1365,12 +1600,14 @@ async def _async_update_configuration(self, service_call): async def _update_configuration_data(self, data: dict): result = False - storage_data_import_keys: dict[str, Callable[[int | bool | str], Awaitable[None]]] = { + storage_data_import_keys: dict[ + str, Callable[[int | bool | str], Awaitable[None]] + ] = { STORAGE_DATA_CONSIDER_AWAY_INTERVAL: self.storage_api.set_consider_away_interval, STORAGE_DATA_UPDATE_ENTITIES_INTERVAL: self.storage_api.set_update_entities_interval, STORAGE_DATA_UPDATE_API_INTERVAL: self.storage_api.set_update_api_interval, STORAGE_DATA_LOG_INCOMING_MESSAGES: self.storage_api.set_log_incoming_messages, - STORAGE_DATA_UNIT: self.storage_api.set_unit + STORAGE_DATA_UNIT: self.storage_api.set_unit, } for key in storage_data_import_keys: diff --git a/custom_components/edgeos/component/models/edge_os_device_data.py b/custom_components/edgeos/component/models/edge_os_device_data.py index d0d7c3e..7150b41 100644 --- a/custom_components/edgeos/component/models/edge_os_device_data.py +++ b/custom_components/edgeos/component/models/edge_os_device_data.py @@ -1,8 +1,23 @@ from __future__ import annotations -from datetime import datetime - -from ..helpers.const import * +from datetime import datetime, timedelta + +from ...core.helpers.const import ENTITY_UNIQUE_ID +from ..helpers.const import ( + DEVICE_DATA_DOMAIN, + DEVICE_DATA_IP, + DEVICE_DATA_MAC, + DEVICE_DATA_NAME, + DEVICE_DATA_RECEIVED, + DEVICE_DATA_SENT, + DHCP_SERVER_LEASED, + RECEIVED_RATE_PREFIX, + RECEIVED_TRAFFIC_PREFIX, + SENT_RATE_PREFIX, + SENT_TRAFFIC_PREFIX, + TRAFFIC_DATA_DIRECTION_RECEIVED, + TRAFFIC_DATA_DIRECTION_SENT, +) from .edge_os_traffic_data import EdgeOSTrafficData @@ -14,7 +29,9 @@ class EdgeOSDeviceData: is_leased: bool traffic: EdgeOSTrafficData - def __init__(self, hostname: str, ip: str, mac: str, domain: str | None, is_leased: bool): + def __init__( + self, hostname: str, ip: str, mac: str, domain: str | None, is_leased: bool + ): self.hostname = hostname self.ip = ip self.mac = mac @@ -32,26 +49,28 @@ def get_stats(self): RECEIVED_RATE_PREFIX: self.received.rate, RECEIVED_TRAFFIC_PREFIX: self.received.total, SENT_RATE_PREFIX: self.sent.rate, - SENT_TRAFFIC_PREFIX: self.sent.total + SENT_TRAFFIC_PREFIX: self.sent.total, } return data @property - def last_activity(self): - received_activity = self.received.last_activity - sent_activity = self.sent.last_activity + def last_activity(self) -> int: + received_activity = int(self.received.last_activity) + sent_activity = int(self.sent.last_activity) - last_activity = received_activity if received_activity > sent_activity else sent_activity + last_activity = ( + received_activity if received_activity > sent_activity else sent_activity + ) return last_activity @property - def last_activity_in_seconds(self) -> float: + def last_activity_in_seconds(self) -> int: now = datetime.now().timestamp() - diff = int(now) - self.last_activity - last_activity_in_seconds = timedelta(seconds=diff).total_seconds() + diff = int(now) - int(self.last_activity) + last_activity_in_seconds = int(timedelta(seconds=diff).total_seconds()) return last_activity_in_seconds @@ -64,7 +83,7 @@ def to_dict(self): DEVICE_DATA_RECEIVED: self.received.to_dict(), DEVICE_DATA_SENT: self.sent.to_dict(), ENTITY_UNIQUE_ID: self.unique_id, - DHCP_SERVER_LEASED: self.is_leased + DHCP_SERVER_LEASED: self.is_leased, } return obj diff --git a/custom_components/edgeos/component/models/edge_os_interface_data.py b/custom_components/edgeos/component/models/edge_os_interface_data.py index 453ba9c..4116c18 100644 --- a/custom_components/edgeos/component/models/edge_os_interface_data.py +++ b/custom_components/edgeos/component/models/edge_os_interface_data.py @@ -1,6 +1,40 @@ from __future__ import annotations -from ..helpers.const import * +from ...core.helpers.const import ENTITY_UNIQUE_ID +from ..helpers.const import ( + IGNORED_INTERFACES, + INTERFACE_DATA_ADDRESS, + INTERFACE_DATA_AGING, + INTERFACE_DATA_BRIDGE_GROUP, + INTERFACE_DATA_BRIDGED_CONNTRACK, + INTERFACE_DATA_DESCRIPTION, + INTERFACE_DATA_DUPLEX, + INTERFACE_DATA_HANDLER, + INTERFACE_DATA_HELLO_TIME, + INTERFACE_DATA_MAX_AGE, + INTERFACE_DATA_MULTICAST, + INTERFACE_DATA_NAME, + INTERFACE_DATA_PRIORITY, + INTERFACE_DATA_PROMISCUOUS, + INTERFACE_DATA_RECEIVED, + INTERFACE_DATA_SENT, + INTERFACE_DATA_SPEED, + INTERFACE_DATA_STP, + INTERFACE_DATA_TYPE, + RECEIVED_DROPPED_PREFIX, + RECEIVED_ERRORS_PREFIX, + RECEIVED_PACKETS_PREFIX, + RECEIVED_RATE_PREFIX, + RECEIVED_TRAFFIC_PREFIX, + SENT_DROPPED_PREFIX, + SENT_ERRORS_PREFIX, + SENT_PACKETS_PREFIX, + SENT_RATE_PREFIX, + SENT_TRAFFIC_PREFIX, + SPECIAL_INTERFACES, + TRAFFIC_DATA_DIRECTION_RECEIVED, + TRAFFIC_DATA_DIRECTION_SENT, +) from ..helpers.enums import InterfaceHandlers from .edge_os_traffic_data import EdgeOSTrafficData @@ -76,7 +110,7 @@ def to_dict(self): INTERFACE_DATA_MULTICAST: self.multicast, INTERFACE_DATA_RECEIVED: self.received.to_dict(), INTERFACE_DATA_SENT: self.sent.to_dict(), - ENTITY_UNIQUE_ID: self.unique_id + ENTITY_UNIQUE_ID: self.unique_id, } return obj @@ -110,7 +144,7 @@ def get_stats(self): SENT_TRAFFIC_PREFIX: self.sent.total, SENT_DROPPED_PREFIX: self.sent.dropped, SENT_ERRORS_PREFIX: self.sent.errors, - SENT_PACKETS_PREFIX: self.sent.packets + SENT_PACKETS_PREFIX: self.sent.packets, } return data diff --git a/custom_components/edgeos/component/models/edge_os_system_data.py b/custom_components/edgeos/component/models/edge_os_system_data.py index 5df7f77..5fe3453 100644 --- a/custom_components/edgeos/component/models/edge_os_system_data.py +++ b/custom_components/edgeos/component/models/edge_os_system_data.py @@ -2,7 +2,18 @@ from datetime import datetime -from ..helpers.const import * +from ..helpers.const import ( + DHCP_SERVER_LEASES, + SYSTEM_DATA_DISABLE, + SYSTEM_DATA_ENABLE, + SYSTEM_DATA_HOSTNAME, + SYSTEM_DATA_LOGIN_USER_LEVEL, + SYSTEM_DATA_NTP, + SYSTEM_DATA_OFFLOAD_HW_NAT, + SYSTEM_DATA_TIME_ZONE, + SYSTEM_DATA_TRAFFIC_ANALYSIS_DPI, + SYSTEM_DATA_TRAFFIC_ANALYSIS_EXPORT, +) class EdgeOSSystemData: @@ -64,7 +75,7 @@ def to_dict(self): SYSTEM_DATA_TRAFFIC_ANALYSIS_DPI: self.deep_packet_inspection, SYSTEM_DATA_TRAFFIC_ANALYSIS_EXPORT: self.traffic_analysis_export, DHCP_SERVER_LEASES: self.leased_devices, - SYSTEM_DATA_LOGIN_USER_LEVEL: self.user_level + SYSTEM_DATA_LOGIN_USER_LEVEL: self.user_level, } return obj diff --git a/custom_components/edgeos/component/models/edge_os_traffic_data.py b/custom_components/edgeos/component/models/edge_os_traffic_data.py index d74b5d1..d3a5770 100644 --- a/custom_components/edgeos/component/models/edge_os_traffic_data.py +++ b/custom_components/edgeos/component/models/edge_os_traffic_data.py @@ -1,8 +1,17 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timedelta -from ..helpers.const import * +from ..helpers.const import ( + TRAFFIC_DATA_DIRECTION, + TRAFFIC_DATA_DROPPED, + TRAFFIC_DATA_ERRORS, + TRAFFIC_DATA_LAST_ACTIVITY, + TRAFFIC_DATA_LAST_ACTIVITY_IN_SECONDS, + TRAFFIC_DATA_PACKETS, + TRAFFIC_DATA_RATE, + TRAFFIC_DATA_TOTAL, +) class EdgeOSTrafficData: @@ -32,18 +41,22 @@ def update(self, data: dict): if self.rate > 0: now = datetime.now().timestamp() - self.last_activity = now + self.last_activity = int(now) def to_dict(self): now = datetime.now().timestamp() - diff = "N/A" if self.last_activity == 0 else timedelta(seconds=(int(now) - self.last_activity)).total_seconds() + diff = ( + "N/A" + if self.last_activity == 0 + else timedelta(seconds=(int(now) - self.last_activity)).total_seconds() + ) obj = { TRAFFIC_DATA_DIRECTION: self.direction, TRAFFIC_DATA_RATE: self.rate, TRAFFIC_DATA_TOTAL: self.total, TRAFFIC_DATA_LAST_ACTIVITY: self.last_activity, - TRAFFIC_DATA_LAST_ACTIVITY_IN_SECONDS: diff + TRAFFIC_DATA_LAST_ACTIVITY_IN_SECONDS: diff, } if self.errors is not None: diff --git a/custom_components/edgeos/config_flow.py b/custom_components/edgeos/config_flow.py index 32081b4..50fabcd 100644 --- a/custom_components/edgeos/config_flow.py +++ b/custom_components/edgeos/config_flow.py @@ -4,13 +4,14 @@ import logging from cryptography.fernet import InvalidToken +import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from .component.api.api import IntegrationAPI -from .component.helpers.const import * +from .configuration.helpers.const import DEFAULT_NAME, DOMAIN from .configuration.helpers.exceptions import AlreadyExistsError, LoginError from .configuration.managers.configuration_manager import ConfigurationManager @@ -56,10 +57,10 @@ async def async_step_user(self, user_input=None): except LoginError as lex: errors = lex.errors - except InvalidToken as itex: + except InvalidToken: errors = {"base": "corrupted_encryption_key"} - except AlreadyExistsError as aeex: + except AlreadyExistsError: errors = {"base": "already_configured"} if errors is not None: @@ -71,11 +72,7 @@ async def async_step_user(self, user_input=None): schema = vol.Schema(new_user_input) - return self.async_show_form( - step_id="user", - data_schema=schema, - errors=errors - ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) class DomainOptionsFlowHandler(config_entries.OptionsFlow): @@ -104,16 +101,20 @@ async def async_step_init(self, user_input=None): _LOGGER.debug("User inputs are valid") - options = self._config_manager.remap_entry_data(self._config_entry, user_input) + options = self._config_manager.remap_entry_data( + self._config_entry, user_input + ) - return self.async_create_entry(title=self._config_entry.title, data=options) + return self.async_create_entry( + title=self._config_entry.title, data=options + ) except LoginError as lex: errors = lex.errors - except InvalidToken as itex: + except InvalidToken: errors = {"base": "corrupted_encryption_key"} - except AlreadyExistsError as aeex: + except AlreadyExistsError: errors = {"base": "already_configured"} if errors is not None: @@ -121,12 +122,10 @@ async def async_step_init(self, user_input=None): _LOGGER.warning(f"Failed to create integration, Error: {error_message}") - new_user_input = self._config_manager.get_options_fields(self._config_entry.data) + new_user_input = self._config_manager.get_options_fields( + self._config_entry.data + ) schema = vol.Schema(new_user_input) - return self.async_show_form( - step_id="init", - data_schema=schema, - errors=errors - ) + return self.async_show_form(step_id="init", data_schema=schema, errors=errors) diff --git a/custom_components/edgeos/configuration/helpers/const.py b/custom_components/edgeos/configuration/helpers/const.py index 909e435..5055adf 100644 --- a/custom_components/edgeos/configuration/helpers/const.py +++ b/custom_components/edgeos/configuration/helpers/const.py @@ -5,17 +5,13 @@ SUPPORTED_PLATFORMS - list of supported HA components to initialize """ -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME DOMAIN = "edgeos" DEFAULT_NAME = "EdgeOS" MANUFACTURER = "Ubiquiti" -DATA_KEYS = [ - CONF_HOST, - CONF_USERNAME, - CONF_PASSWORD -] +DATA_KEYS = [CONF_HOST, CONF_USERNAME, CONF_PASSWORD] MAXIMUM_RECONNECT = 3 diff --git a/custom_components/edgeos/configuration/managers/configuration_manager.py b/custom_components/edgeos/configuration/managers/configuration_manager.py index e447396..71d21aa 100644 --- a/custom_components/edgeos/configuration/managers/configuration_manager.py +++ b/custom_components/edgeos/configuration/managers/configuration_manager.py @@ -7,12 +7,13 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from ...core.api.base_api import BaseAPI -from ...core.helpers.const import * from ...core.helpers.enums import ConnectivityStatus from ...core.managers.password_manager import PasswordManager +from ..helpers.const import DATA_KEYS from ..helpers.exceptions import LoginError from ..models.config_data import ConfigData @@ -128,7 +129,9 @@ def get_options_fields(self, user_input: dict[str, Any]) -> dict[vol.Marker, Any return fields - def remap_entry_data(self, entry: ConfigEntry, options: dict[str, Any]) -> dict[str, Any]: + def remap_entry_data( + self, entry: ConfigEntry, options: dict[str, Any] + ) -> dict[str, Any]: config_options = {} config_data = {} diff --git a/custom_components/edgeos/configuration/models/config_data.py b/custom_components/edgeos/configuration/models/config_data.py index 5e54474..5ea9ad2 100644 --- a/custom_components/edgeos/configuration/models/config_data.py +++ b/custom_components/edgeos/configuration/models/config_data.py @@ -3,8 +3,9 @@ from typing import Any from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from ..helpers.const import * +from ..helpers.const import API_URL_TEMPLATE class ConfigData: @@ -40,7 +41,7 @@ def to_dict(self): obj = { CONF_HOST: self.host, CONF_USERNAME: self.username, - CONF_PASSWORD: self.password + CONF_PASSWORD: self.password, } return obj diff --git a/custom_components/edgeos/core/api/base_api.py b/custom_components/edgeos/core/api/base_api.py index 8586142..53c6035 100644 --- a/custom_components/edgeos/core/api/base_api.py +++ b/custom_components/edgeos/core/api/base_api.py @@ -24,12 +24,13 @@ class BaseAPI: onDataChangedAsync: Callable[[], Awaitable[None]] | None = None onStatusChangedAsync: Callable[[ConnectivityStatus], Awaitable[None]] | None = None - def __init__(self, - hass: HomeAssistant | None, - async_on_data_changed: Callable[[], Awaitable[None]] | None = None, - async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] | None = None - ): - + def __init__( + self, + hass: HomeAssistant | None, + async_on_data_changed: Callable[[], Awaitable[None]] | None = None, + async_on_status_changed: Callable[[ConnectivityStatus], Awaitable[None]] + | None = None, + ): self.hass = hass self.status = ConnectivityStatus.NotConnected self.data = {} @@ -45,7 +46,9 @@ def is_home_assistant(self): async def initialize_session(self, cookies=None, cookie_jar=None): try: if self.is_home_assistant: - self.session = async_create_clientsession(hass=self.hass, cookies=cookies, cookie_jar=cookie_jar) + self.session = async_create_clientsession( + hass=self.hass, cookies=cookies, cookie_jar=cookie_jar + ) else: self.session = ClientSession(cookies=cookies, cookie_jar=cookie_jar) @@ -56,7 +59,9 @@ async def initialize_session(self, cookies=None, cookie_jar=None): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.warning(f"Failed to initialize session, Error: {str(ex)}, Line: {line_number}") + _LOGGER.warning( + f"Failed to initialize session, Error: {str(ex)}, Line: {line_number}" + ) await self.set_status(ConnectivityStatus.Failed) diff --git a/custom_components/edgeos/core/components/binary_sensor.py b/custom_components/edgeos/core/components/binary_sensor.py index 753a11b..979292e 100644 --- a/custom_components/edgeos/core/components/binary_sensor.py +++ b/custom_components/edgeos/core/components/binary_sensor.py @@ -7,13 +7,14 @@ from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import DOMAIN_BINARY_SENSOR from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData class CoreBinarySensor(BinarySensorEntity, BaseEntity): """Representation a binary sensor that is updated.""" + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/custom_components/edgeos/core/components/camera.py b/custom_components/edgeos/core/components/camera.py index 027d7e9..2e7e9b5 100644 --- a/custom_components/edgeos/core/components/camera.py +++ b/custom_components/edgeos/core/components/camera.py @@ -15,10 +15,19 @@ from homeassistant.components.camera import DEFAULT_CONTENT_TYPE, SUPPORT_STREAM, Camera from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from ..helpers.const import * +import homeassistant.helpers.config_validation as cv + +from ..helpers.const import ( + ATTR_MODE_RECORD, + ATTR_STREAM_FPS, + CONF_MOTION_DETECTION, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DOMAIN_CAMERA, + EMPTY_STRING, + SINGLE_FRAME_PS, +) from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData @@ -26,7 +35,7 @@ class CoreCamera(Camera, BaseEntity, ABC): - """ Camera """ + """Camera""" def __init__(self, hass, device_info): super().__init__() @@ -47,12 +56,13 @@ def initialize( hass: HomeAssistant, entity: EntityData, current_domain: str, + DOMAIN_STREAM=None, ): super().initialize(hass, entity, current_domain) try: if self.ha is None: - _LOGGER.warning(f"Failed to initialize CoreCamera without HA manager") + _LOGGER.warning("Failed to initialize CoreCamera without HA manager") return config_data = self.ha.config_data @@ -72,7 +82,9 @@ def initialize( stream_support = DOMAIN_STREAM in self.hass.data - stream_support_flag = SUPPORT_STREAM if stream_source and stream_support else 0 + stream_support_flag = ( + SUPPORT_STREAM if stream_source and stream_support else 0 + ) self._still_image_url = still_image_url_template self._still_image_url.hass = hass @@ -90,7 +102,9 @@ def initialize( exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to initialize CoreCamera instance, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to initialize CoreCamera instance, Error: {ex}, Line: {line_number}" + ) @property def is_recording(self) -> bool: @@ -110,18 +124,24 @@ def frame_interval(self): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image(self, width: int | None = None, height: int | None = None) -> bytes | None: + def camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return bytes of camera image.""" return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() - async def async_camera_image(self, width: int | None = None, height: int | None = None) -> bytes | None: + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: """Return a still image response from the camera.""" try: url = self._still_image_url.async_render() except TemplateError as err: - _LOGGER.error(f"Error parsing template {self._still_image_url}, Error: {err}") + _LOGGER.error( + f"Error parsing template {self._still_image_url}, Error: {err}" + ) return self._last_image try: @@ -137,7 +157,9 @@ async def async_camera_image(self, width: int | None = None, height: int | None return self._last_image except aiohttp.ClientError as err: - _LOGGER.error(f"Error getting new camera image from {self.name}, Error: {err}") + _LOGGER.error( + f"Error getting new camera image from {self.name}, Error: {err}" + ) return self._last_image self._last_url = url diff --git a/custom_components/edgeos/core/components/device_tracker.py b/custom_components/edgeos/core/components/device_tracker.py index 3977909..9216d81 100644 --- a/custom_components/edgeos/core/components/device_tracker.py +++ b/custom_components/edgeos/core/components/device_tracker.py @@ -10,7 +10,7 @@ from homeassistant.components.device_tracker.const import ATTR_IP, ATTR_MAC from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import DOMAIN_DEVICE_TRACKER from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData diff --git a/custom_components/edgeos/core/components/light.py b/custom_components/edgeos/core/components/light.py index d00eecc..769269c 100644 --- a/custom_components/edgeos/core/components/light.py +++ b/custom_components/edgeos/core/components/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ColorMode, LightEntity from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import DOMAIN_LIGHT from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData diff --git a/custom_components/edgeos/core/components/select.py b/custom_components/edgeos/core/components/select.py index fffb7d3..38f42d4 100644 --- a/custom_components/edgeos/core/components/select.py +++ b/custom_components/edgeos/core/components/select.py @@ -10,7 +10,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import ATTR_OPTIONS, DOMAIN_SELECT from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData @@ -18,7 +18,8 @@ class CoreSelect(SelectEntity, BaseEntity, ABC): - """ Core Select """ + """Core Select""" + def initialize( self, hass: HomeAssistant, @@ -35,7 +36,9 @@ def initialize( exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to initialize CoreSelect instance, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to initialize CoreSelect instance, Error: {ex}, Line: {line_number}" + ) @property def current_option(self) -> str: diff --git a/custom_components/edgeos/core/components/sensor.py b/custom_components/edgeos/core/components/sensor.py index e520702..cc7dde1 100644 --- a/custom_components/edgeos/core/components/sensor.py +++ b/custom_components/edgeos/core/components/sensor.py @@ -1,7 +1,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import DOMAIN_SENSOR from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData diff --git a/custom_components/edgeos/core/components/switch.py b/custom_components/edgeos/core/components/switch.py index dd6d4d7..21b57e9 100644 --- a/custom_components/edgeos/core/components/switch.py +++ b/custom_components/edgeos/core/components/switch.py @@ -8,7 +8,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import DOMAIN_SWITCH from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData diff --git a/custom_components/edgeos/core/components/vacuum.py b/custom_components/edgeos/core/components/vacuum.py index 5080031..262541e 100644 --- a/custom_components/edgeos/core/components/vacuum.py +++ b/custom_components/edgeos/core/components/vacuum.py @@ -8,7 +8,7 @@ from homeassistant.components.vacuum import StateVacuumEntity from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import ATTR_FANS_SPEED_LIST, ATTR_FEATURES, DOMAIN_VACUUM from ..models.base_entity import BaseEntity from ..models.entity_data import EntityData @@ -28,16 +28,22 @@ def initialize( try: if hasattr(self.entity_description, ATTR_FEATURES): - self._attr_supported_features = getattr(self.entity_description, ATTR_FEATURES) + self._attr_supported_features = getattr( + self.entity_description, ATTR_FEATURES + ) if hasattr(self.entity_description, ATTR_FANS_SPEED_LIST): - self._attr_fan_speed_list = getattr(self.entity_description, ATTR_FANS_SPEED_LIST) + self._attr_fan_speed_list = getattr( + self.entity_description, ATTR_FANS_SPEED_LIST + ) except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to initialize CoreSelect instance, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to initialize CoreSelect instance, Error: {ex}, Line: {line_number}" + ) @property def state(self) -> str | None: @@ -75,10 +81,10 @@ async def async_toggle(self, **kwargs: Any) -> None: await self.ha.async_core_entity_toggle(self.entity) async def async_send_command( - self, - command: str, - params: dict[str, Any] | list[Any] | None = None, - **kwargs: Any, + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, ) -> None: """Send a command to a vacuum cleaner.""" await self.ha.async_core_entity_send_command(self.entity, command, params) diff --git a/custom_components/edgeos/core/helpers/__init__.py b/custom_components/edgeos/core/helpers/__init__.py index f22bae1..13e4d8c 100644 --- a/custom_components/edgeos/core/helpers/__init__.py +++ b/custom_components/edgeos/core/helpers/__init__.py @@ -1,10 +1,10 @@ from homeassistant.core import HomeAssistant -from ...core.helpers.const import * +from ...core.helpers.const import DATA def get_ha(hass: HomeAssistant, entry_id): - ha_data = hass.data.get(DATA, dict()) + ha_data = hass.data.get(DATA, {}) ha = ha_data.get(entry_id) return ha diff --git a/custom_components/edgeos/core/helpers/const.py b/custom_components/edgeos/core/helpers/const.py index 1a8a203..a0b4616 100644 --- a/custom_components/edgeos/core/helpers/const.py +++ b/custom_components/edgeos/core/helpers/const.py @@ -2,14 +2,12 @@ from homeassistant.components.camera import DOMAIN as DOMAIN_CAMERA from homeassistant.components.device_tracker import DOMAIN as DOMAIN_DEVICE_TRACKER from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT -from homeassistant.components.media_source import DOMAIN as DOMAIN_MEDIA_SOURCE from homeassistant.components.select import DOMAIN as DOMAIN_SELECT from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR -from homeassistant.components.stream import DOMAIN as DOMAIN_STREAM from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.components.vacuum import DOMAIN as DOMAIN_VACUUM -from ...configuration.helpers.const import * +from ...configuration.helpers.const import DOMAIN SUPPORTED_PLATFORMS = [ DOMAIN_BINARY_SENSOR, @@ -19,10 +17,12 @@ DOMAIN_VACUUM, DOMAIN_SENSOR, DOMAIN_LIGHT, - DOMAIN_DEVICE_TRACKER + DOMAIN_DEVICE_TRACKER, ] -PLATFORMS = {domain: f"{DOMAIN}_{domain}_UPDATE_SIGNAL" for domain in SUPPORTED_PLATFORMS} +PLATFORMS = { + domain: f"{DOMAIN}_{domain}_UPDATE_SIGNAL" for domain in SUPPORTED_PLATFORMS +} ENTITY_STATE = "state" ENTITY_ATTRIBUTES = "attributes" diff --git a/custom_components/edgeos/core/helpers/setup_base_entry.py b/custom_components/edgeos/core/helpers/setup_base_entry.py index e980ca6..3c4f073 100644 --- a/custom_components/edgeos/core/helpers/setup_base_entry.py +++ b/custom_components/edgeos/core/helpers/setup_base_entry.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from ..helpers.const import * +from ..helpers.const import DATA from ..models.domain_data import DomainData from ..models.entity_data import EntityData @@ -25,7 +25,7 @@ async def async_setup_base_entry( _LOGGER.debug(f"Starting async_setup_entry {domain}") try: - ha_data = hass.data.get(DATA, dict()) + ha_data = hass.data.get(DATA, {}) ha = ha_data.get(entry.entry_id) diff --git a/custom_components/edgeos/core/managers/entity_manager.py b/custom_components/edgeos/core/managers/entity_manager.py index 295090f..e4b1886 100644 --- a/custom_components/edgeos/core/managers/entity_manager.py +++ b/custom_components/edgeos/core/managers/entity_manager.py @@ -9,7 +9,7 @@ from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntryDisabler -from ..helpers.const import * +from ..helpers.const import DOMAIN from ..helpers.enums import EntityStatus from ..models.domain_data import DomainData from ..models.entity_data import EntityData @@ -18,7 +18,7 @@ class EntityManager: - """ Entity Manager is agnostic to component - PLEASE DON'T CHANGE """ + """Entity Manager is agnostic to component - PLEASE DON'T CHANGE""" hass: HomeAssistant domain_component_manager: dict[str, DomainData] @@ -56,8 +56,9 @@ async def _handle_disabled_entity(self, entity_id, entity: EntityData): if entity.disabled: _LOGGER.info(f"Disabling entity, Data: {entity}") - self.entity_registry.async_update_entity(entity_id, - disabled_by=RegistryEntryDisabler.INTEGRATION) + self.entity_registry.async_update_entity( + entity_id, disabled_by=RegistryEntryDisabler.INTEGRATION + ) else: entity.disabled = entity_item.disabled @@ -169,19 +170,19 @@ async def _async_update(self): line_number = tb.tb_lineno _LOGGER.error( - f"Failed to update, " - f"Error: {str(ex)}, " - f"Line: {line_number}" + f"Failed to update, " f"Error: {str(ex)}, " f"Line: {line_number}" ) - def _compare_data(self, - entity_name: str, - entity: EntityData, - state: str | int | float | bool, - attributes: dict, - device_name: str, - entity_description: EntityDescription | None = None, - details: dict | None = None): + def _compare_data( + self, + entity_name: str, + entity: EntityData, + state: str | int | float | bool, + attributes: dict, + device_name: str, + entity_description: EntityDescription | None = None, + details: dict | None = None, + ): msgs = [] if str(entity.state) != str(state): @@ -196,8 +197,13 @@ def _compare_data(self, if entity.device_name != device_name: msgs.append(f"Device name {entity.device_name} -> {device_name}") - if entity_description is not None and entity.entity_description != entity_description: - msgs.append(f"Description {str(entity.entity_description)} -> {str(entity_description)}") + if ( + entity_description is not None + and entity.entity_description != entity_description + ): + msgs.append( + f"Description {str(entity.entity_description)} -> {str(entity_description)}" + ) if details is not None and entity.details != details: from_details = self._get_attributes_json(entity.details) @@ -230,17 +236,17 @@ def get(self, unique_id: str) -> EntityData | None: return entity - def set_entity(self, - domain: str, - entry_id: str, - state: str | int | float | bool | datetime, - attributes: dict, - device_name: str, - entity_description: EntityDescription | None, - details: dict | None = None, - destructors: list[bool] = None - ): - + def set_entity( + self, + domain: str, + entry_id: str, + state: str | int | float | bool | datetime, + attributes: dict, + device_name: str, + entity_description: EntityDescription | None, + details: dict | None = None, + destructors: list[bool] = None, + ): try: entity = self.entities.get(entity_description.key) entity_name = entity_description.name @@ -260,17 +266,21 @@ def set_entity(self, entity.status = EntityStatus.CREATED entity.domain = domain - self._compare_data(entity_name, entity, state, attributes, device_name) + self._compare_data( + entity_name, entity, state, attributes, device_name + ) else: original_status = entity.status - was_modified = self._compare_data(entity_name, - entity, - state, - attributes, - device_name, - entity_description, - details) + was_modified = self._compare_data( + entity_name, + entity, + state, + attributes, + device_name, + entity_description, + details, + ) if was_modified: entity.status = EntityStatus.UPDATED diff --git a/custom_components/edgeos/core/managers/home_assistant.py b/custom_components/edgeos/core/managers/home_assistant.py index 4eb79e5..95555c4 100644 --- a/custom_components/edgeos/core/managers/home_assistant.py +++ b/custom_components/edgeos/core/managers/home_assistant.py @@ -16,7 +16,24 @@ from homeassistant.helpers.entity_registry import EntityRegistry, async_get from homeassistant.helpers.event import async_track_time_interval -from ..helpers.const import * +from ..helpers.const import ( + ACTION_CORE_ENTITY_DISABLE_MOTION_DETECTION, + ACTION_CORE_ENTITY_ENABLE_MOTION_DETECTION, + ACTION_CORE_ENTITY_LOCATE, + ACTION_CORE_ENTITY_PAUSE, + ACTION_CORE_ENTITY_RETURN_TO_BASE, + ACTION_CORE_ENTITY_SELECT_OPTION, + ACTION_CORE_ENTITY_SEND_COMMAND, + ACTION_CORE_ENTITY_SET_FAN_SPEED, + ACTION_CORE_ENTITY_START, + ACTION_CORE_ENTITY_STOP, + ACTION_CORE_ENTITY_TOGGLE, + ACTION_CORE_ENTITY_TURN_OFF, + ACTION_CORE_ENTITY_TURN_ON, + DOMAIN, + PLATFORMS, + SUPPORTED_PLATFORMS, +) from ..managers.device_manager import DeviceManager from ..managers.entity_manager import EntityManager from ..managers.storage_manager import StorageManager @@ -26,12 +43,12 @@ class HomeAssistantManager: - def __init__(self, - hass: HomeAssistant, - scan_interval: datetime.timedelta, - heartbeat_interval: datetime.timedelta | None = None - ): - + def __init__( + self, + hass: HomeAssistant, + scan_interval: datetime.timedelta, + heartbeat_interval: datetime.timedelta | None = None, + ): self._hass = hass self._is_initialized = False @@ -61,7 +78,9 @@ def _send_heartbeat(internal_now): self._send_heartbeat = _send_heartbeat - self._domains = {domain: self.is_domain_supported(domain) for domain in SUPPORTED_PLATFORMS} + self._domains = { + domain: self.is_domain_supported(domain) for domain in SUPPORTED_PLATFORMS + } @property def entity_manager(self) -> EntityManager: @@ -90,45 +109,35 @@ def entry_id(self) -> str: def entry_title(self) -> str: return self._entry.title - def update_intervals(self, - entities_interval: datetime.timedelta, - data_interval: datetime.timedelta - ): - + def update_intervals( + self, entities_interval: datetime.timedelta, data_interval: datetime.timedelta + ): self._update_entities_interval = entities_interval self._update_data_providers_interval = data_interval async def async_component_initialize(self, entry: ConfigEntry): - """ Component initialization """ - pass + """Component initialization""" async def async_send_heartbeat(self): - """ Must be implemented to be able to send heartbeat to API """ - pass + """Must be implemented to be able to send heartbeat to API""" def register_services(self, entry: ConfigEntry | None = None): - """ Must be implemented to be able to expose services """ - pass + """Must be implemented to be able to expose services""" async def async_initialize_data_providers(self): - """ Must be implemented to be able to send heartbeat to API """ - pass + """Must be implemented to be able to send heartbeat to API""" async def async_stop_data_providers(self): - """ Must be implemented to be able to send heartbeat to API """ - pass + """Must be implemented to be able to send heartbeat to API""" async def async_update_data_providers(self): - """ Must be implemented to be able to send heartbeat to API """ - pass + """Must be implemented to be able to send heartbeat to API""" def load_entities(self): - """ Must be implemented to be able to send heartbeat to API """ - pass + """Must be implemented to be able to send heartbeat to API""" def load_devices(self): - """ Must be implemented to be able to send heartbeat to API """ - pass + """Must be implemented to be able to send heartbeat to API""" async def async_init(self, entry: ConfigEntry): try: @@ -185,7 +194,9 @@ async def async_update_entry(self, entry: ConfigEntry | None = None): entry = self._entry track_time_update_data_providers = async_track_time_interval( - self._hass, self._update_data_providers, self._update_data_providers_interval + self._hass, + self._update_data_providers, + self._update_data_providers_interval, ) self._async_track_time_handlers.append(track_time_update_data_providers) @@ -208,7 +219,7 @@ async def async_update_entry(self, entry: ConfigEntry | None = None): await self.async_initialize_data_providers() async def async_unload(self): - _LOGGER.info(f"HA was stopped") + _LOGGER.info("HA was stopped") for handler in self._async_track_time_handlers: if handler is not None: @@ -254,7 +265,9 @@ def _update_entities(self, now): exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to update devices and entities, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to update devices and entities, Error: {ex}, Line: {line_number}" + ) self.entity_manager.update() @@ -287,96 +300,104 @@ def get_core_entity_fan_speed(self, entity: EntityData) -> str | None: pass async def async_core_entity_return_to_base(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_RETURN_TO_BASE. """ + """Handles ACTION_CORE_ENTITY_RETURN_TO_BASE.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_RETURN_TO_BASE) if action is not None: await action(entity) - async def async_core_entity_set_fan_speed(self, entity: EntityData, fan_speed: str) -> None: - """ Handles ACTION_CORE_ENTITY_SET_FAN_SPEED. """ + async def async_core_entity_set_fan_speed( + self, entity: EntityData, fan_speed: str + ) -> None: + """Handles ACTION_CORE_ENTITY_SET_FAN_SPEED.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_SET_FAN_SPEED) if action is not None: await action(entity, fan_speed) async def async_core_entity_start(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_START. """ + """Handles ACTION_CORE_ENTITY_START.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_START) if action is not None: await action(entity) async def async_core_entity_stop(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_STOP. """ + """Handles ACTION_CORE_ENTITY_STOP.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_STOP) if action is not None: await action(entity) async def async_core_entity_pause(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_PAUSE. """ + """Handles ACTION_CORE_ENTITY_PAUSE.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_PAUSE) if action is not None: await action(entity) async def async_core_entity_turn_on(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_TURN_ON. """ + """Handles ACTION_CORE_ENTITY_TURN_ON.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_TURN_ON) if action is not None: await action(entity) async def async_core_entity_turn_off(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_TURN_OFF. """ + """Handles ACTION_CORE_ENTITY_TURN_OFF.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_TURN_OFF) if action is not None: await action(entity) async def async_core_entity_send_command( - self, - entity: EntityData, - command: str, - params: dict[str, Any] | list[Any] | None = None + self, + entity: EntityData, + command: str, + params: dict[str, Any] | list[Any] | None = None, ) -> None: - """ Handles ACTION_CORE_ENTITY_SEND_COMMAND. """ + """Handles ACTION_CORE_ENTITY_SEND_COMMAND.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_SEND_COMMAND) if action is not None: await action(entity, command, params) async def async_core_entity_locate(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_LOCATE. """ + """Handles ACTION_CORE_ENTITY_LOCATE.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_LOCATE) if action is not None: await action(entity) - async def async_core_entity_select_option(self, entity: EntityData, option: str) -> None: - """ Handles ACTION_CORE_ENTITY_SELECT_OPTION. """ + async def async_core_entity_select_option( + self, entity: EntityData, option: str + ) -> None: + """Handles ACTION_CORE_ENTITY_SELECT_OPTION.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_SELECT_OPTION) if action is not None: await action(entity, option) async def async_core_entity_toggle(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_TOGGLE. """ + """Handles ACTION_CORE_ENTITY_TOGGLE.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_TOGGLE) if action is not None: await action(entity) - async def async_core_entity_enable_motion_detection(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_ENABLE_MOTION_DETECTION. """ + async def async_core_entity_enable_motion_detection( + self, entity: EntityData + ) -> None: + """Handles ACTION_CORE_ENTITY_ENABLE_MOTION_DETECTION.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_ENABLE_MOTION_DETECTION) if action is not None: await action(entity) - async def async_core_entity_disable_motion_detection(self, entity: EntityData) -> None: - """ Handles ACTION_CORE_ENTITY_DISABLE_MOTION_DETECTION. """ + async def async_core_entity_disable_motion_detection( + self, entity: EntityData + ) -> None: + """Handles ACTION_CORE_ENTITY_DISABLE_MOTION_DETECTION.""" action = self.get_action(entity.id, ACTION_CORE_ENTITY_DISABLE_MOTION_DETECTION) if action is not None: @@ -395,7 +416,8 @@ def is_domain_supported(domain) -> bool: try: __import__(f"custom_components.{DOMAIN}.{domain}") - except ModuleNotFoundError as mnfe: + + except ModuleNotFoundError: is_supported = False return is_supported diff --git a/custom_components/edgeos/core/managers/password_manager.py b/custom_components/edgeos/core/managers/password_manager.py index bb3c4de..3466679 100644 --- a/custom_components/edgeos/core/managers/password_manager.py +++ b/custom_components/edgeos/core/managers/password_manager.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant -from ..helpers.const import * +from ..helpers.const import DOMAIN_KEY_FILE from ..managers.storage_manager import StorageManager from ..models.storage_data import StorageData diff --git a/custom_components/edgeos/core/managers/storage_manager.py b/custom_components/edgeos/core/managers/storage_manager.py index 7de71c2..d0ac584 100644 --- a/custom_components/edgeos/core/managers/storage_manager.py +++ b/custom_components/edgeos/core/managers/storage_manager.py @@ -4,7 +4,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store -from ..helpers.const import * +from ..helpers.const import DOMAIN, STORAGE_VERSION from ..models.storage_data import StorageData _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/edgeos/core/models/base_entity.py b/custom_components/edgeos/core/models/base_entity.py index 8c2f438..bc7b12e 100644 --- a/custom_components/edgeos/core/models/base_entity.py +++ b/custom_components/edgeos/core/models/base_entity.py @@ -7,7 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from ..helpers.const import * +from ..helpers.const import DATA, PLATFORMS from ..managers.device_manager import DeviceManager from ..managers.entity_manager import EntityManager from ..models.entity_data import EntityData @@ -39,7 +39,7 @@ def initialize( self.remove_dispatcher = None self.current_domain = current_domain - ha_data = hass.data.get(DATA, dict()) + ha_data = hass.data.get(DATA, {}) self.ha = ha_data.get(entity.entry_id) @@ -55,7 +55,9 @@ def initialize( exc_type, exc_obj, tb = sys.exc_info() line_number = tb.tb_lineno - _LOGGER.error(f"Failed to initialize BaseEntity, Error: {ex}, Line: {line_number}") + _LOGGER.error( + f"Failed to initialize BaseEntity, Error: {ex}, Line: {line_number}" + ) @property def entry_id(self) -> str | None: diff --git a/custom_components/edgeos/core/models/domain_data.py b/custom_components/edgeos/core/models/domain_data.py index 42c9e78..eec7444 100644 --- a/custom_components/edgeos/core/models/domain_data.py +++ b/custom_components/edgeos/core/models/domain_data.py @@ -8,7 +8,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from ..helpers.const import * from ..models.entity_data import EntityData _LOGGER = logging.getLogger(__name__) @@ -20,10 +19,10 @@ class DomainData: initializer: Callable[[HomeAssistant, EntityData], Any] def __init__( - self, - name, - async_add_devices: AddEntitiesCallback, - initializer: Callable[[HomeAssistant, EntityData], Any] + self, + name, + async_add_devices: AddEntitiesCallback, + initializer: Callable[[HomeAssistant, EntityData], Any], ): self.name = name self.async_add_devices = async_add_devices @@ -32,9 +31,7 @@ def __init__( _LOGGER.info(f"Creating domain data for {name}") def __repr__(self): - obj = { - CONF_NAME: self.name - } + obj = {CONF_NAME: self.name} to_string = f"{obj}" diff --git a/custom_components/edgeos/core/models/entity_data.py b/custom_components/edgeos/core/models/entity_data.py index c785ee4..15f91df 100644 --- a/custom_components/edgeos/core/models/entity_data.py +++ b/custom_components/edgeos/core/models/entity_data.py @@ -5,7 +5,17 @@ from homeassistant.helpers.entity import EntityDescription from homeassistant.util import slugify -from ..helpers.const import * +from ..helpers.const import ( + ENTITY_ATTRIBUTES, + ENTITY_CONFIG_ENTRY_ID, + ENTITY_DETAILS, + ENTITY_DEVICE_NAME, + ENTITY_DISABLED, + ENTITY_DOMAIN, + ENTITY_STATE, + ENTITY_STATUS, + ENTITY_UNIQUE_ID, +) from ..helpers.enums import EntityStatus @@ -49,7 +59,7 @@ def __repr__(self): ENTITY_STATUS: self.status, ENTITY_DISABLED: self.disabled, ENTITY_DOMAIN: self.domain, - ENTITY_CONFIG_ENTRY_ID: self.entry_id + ENTITY_CONFIG_ENTRY_ID: self.entry_id, } to_string = f"{obj}" diff --git a/custom_components/edgeos/core/models/storage_data.py b/custom_components/edgeos/core/models/storage_data.py index 4b5e2d5..c97c15b 100644 --- a/custom_components/edgeos/core/models/storage_data.py +++ b/custom_components/edgeos/core/models/storage_data.py @@ -17,9 +17,7 @@ def from_dict(obj: dict): return data def to_dict(self): - obj = { - "key": self.key - } + obj = {"key": self.key} return obj diff --git a/custom_components/edgeos/device_tracker.py b/custom_components/edgeos/device_tracker.py index 37ebf3f..f2e2408 100644 --- a/custom_components/edgeos/device_tracker.py +++ b/custom_components/edgeos/device_tracker.py @@ -15,7 +15,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the entity.""" await async_setup_base_entry( - hass, config_entry, async_add_devices, CoreScanner.get_domain(), CoreScanner.get_component + hass, + config_entry, + async_add_devices, + CoreScanner.get_domain(), + CoreScanner.get_component, ) diff --git a/custom_components/edgeos/diagnostics.py b/custom_components/edgeos/diagnostics.py index 97c4f2f..96c6cde 100644 --- a/custom_components/edgeos/diagnostics.py +++ b/custom_components/edgeos/diagnostics.py @@ -10,13 +10,13 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from . import ( +from .component.helpers import get_ha +from .component.helpers.const import ( API_DATA_INTERFACES, API_DATA_SYSTEM, DEVICE_LIST, MESSAGES_COUNTER_SECTION, ) -from .component.helpers import get_ha from .component.managers.home_assistant import EdgeOSHomeAssistantManager from .configuration.helpers.const import DOMAIN @@ -85,9 +85,13 @@ def _async_get_diagnostics( network_device = device_list.get(network_device_id) if manager.get_device_name(network_device) == device_name: - _LOGGER.debug(f"Getting diagnostic information for device #{network_device.unique_id}") + _LOGGER.debug( + f"Getting diagnostic information for device #{network_device.unique_id}" + ) - data |= _async_device_as_dict(hass, network_device.to_dict(), network_device.unique_id) + data |= _async_device_as_dict( + hass, network_device.to_dict(), network_device.unique_id + ) break @@ -96,9 +100,13 @@ def _async_get_diagnostics( interface = interfaces.get(unique_id) if manager.get_interface_name(interface) == device_name: - _LOGGER.debug(f"Getting diagnostic information for interface #{interface.unique_id}") + _LOGGER.debug( + f"Getting diagnostic information for interface #{interface.unique_id}" + ) - data |= _async_device_as_dict(hass, interface.to_dict(), interface.unique_id) + data |= _async_device_as_dict( + hass, interface.to_dict(), interface.unique_id + ) break else: @@ -106,14 +114,22 @@ def _async_get_diagnostics( data.update( devices=[ - _async_device_as_dict(hass, device_list[device_id].to_dict(), device_list[device_id].unique_id) + _async_device_as_dict( + hass, + device_list[device_id].to_dict(), + device_list[device_id].unique_id, + ) for device_id in device_list ], interfaces=[ - _async_device_as_dict(hass, interfaces[interface_id].to_dict(), interfaces[interface_id].unique_id) + _async_device_as_dict( + hass, + interfaces[interface_id].to_dict(), + interfaces[interface_id].unique_id, + ) for interface_id in interfaces ], - system=_async_device_as_dict(hass, system.to_dict(), manager.system_name) + system=_async_device_as_dict(hass, system.to_dict(), manager.system_name), ) return data @@ -121,10 +137,8 @@ def _async_get_diagnostics( @callback def _async_device_as_dict( - hass: HomeAssistant, - data: dict, - unique_id: str) -> dict[str, Any]: - + hass: HomeAssistant, data: dict, unique_id: str +) -> dict[str, Any]: """Represent a Shinobi monitor as a dictionary.""" device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) diff --git a/custom_components/edgeos/manifest.json b/custom_components/edgeos/manifest.json index b86b366..c8c0b47 100644 --- a/custom_components/edgeos/manifest.json +++ b/custom_components/edgeos/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/elad-bar/ha-edgeos/issues", "requirements": ["aiohttp"], - "version": "2.0.25" + "version": "2.0.26" } diff --git a/custom_components/edgeos/select.py b/custom_components/edgeos/select.py index a5450ff..facfc33 100644 --- a/custom_components/edgeos/select.py +++ b/custom_components/edgeos/select.py @@ -14,7 +14,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the component.""" await async_setup_base_entry( - hass, config_entry, async_add_devices, CoreSelect.get_domain(), CoreSelect.get_component + hass, + config_entry, + async_add_devices, + CoreSelect.get_domain(), + CoreSelect.get_component, ) diff --git a/custom_components/edgeos/sensor.py b/custom_components/edgeos/sensor.py index d55cf0f..f57921a 100644 --- a/custom_components/edgeos/sensor.py +++ b/custom_components/edgeos/sensor.py @@ -14,7 +14,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Sensor.""" await async_setup_base_entry( - hass, config_entry, async_add_devices, CoreSensor.get_domain(), CoreSensor.get_component + hass, + config_entry, + async_add_devices, + CoreSensor.get_domain(), + CoreSensor.get_component, ) diff --git a/custom_components/edgeos/services.yaml b/custom_components/edgeos/services.yaml index a0aa16e..7c8ae65 100644 --- a/custom_components/edgeos/services.yaml +++ b/custom_components/edgeos/services.yaml @@ -1,4 +1,3 @@ - update_configuration: name: Update configuration description: Update configuration of EdgeOS integration diff --git a/custom_components/edgeos/switch.py b/custom_components/edgeos/switch.py index 6dd79d7..5ef5c99 100644 --- a/custom_components/edgeos/switch.py +++ b/custom_components/edgeos/switch.py @@ -14,7 +14,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Switch.""" await async_setup_base_entry( - hass, config_entry, async_add_devices, CoreSwitch.get_domain(), CoreSwitch.get_component + hass, + config_entry, + async_add_devices, + CoreSwitch.get_domain(), + CoreSwitch.get_component, ) diff --git a/info.md b/info.md index df53797..b94c198 100644 --- a/info.md +++ b/info.md @@ -16,8 +16,9 @@ Provides an integration between EdgeOS (Ubiquiti) routers to Home Assistant. - To enable / disable interfaces an `admin` role is a required #### Installations via HACS + - In HACS, look for "Ubiquiti EdgeOS Routers" and install and restart -- In Settings --> Devices & Services - (Lower Right) "Add Integration" +- In Settings --> Devices & Services - (Lower Right) "Add Integration" #### Setup @@ -25,7 +26,7 @@ To add integration use Configuration -> Integrations -> Add `EdgeOS` Integration supports **multiple** EdgeOS devices | Fields name | Type | Required | Default | Description | -|-------------|---------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| ----------- | ------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Host | Textbox | + | - | Hostname or IP address to access EdgeOS device, can hold also port (HOST:PORT), default port is 443 | | Username | Textbox | + | - | Username of user with `Operator` level access or higher, better to create a dedicated user for that integration for faster issues identification | | Password | Textbox | + | - | | @@ -33,7 +34,7 @@ Integration supports **multiple** EdgeOS devices ###### EdgeOS Device validation errors | Errors | -|------------------------------------------------------------------------------------| +| ---------------------------------------------------------------------------------- | | Cannot reach device (404) | | Invalid credentials (403) | | General authentication error (when failed to get valid response from device) | @@ -58,7 +59,7 @@ Please remove the integration and re-add it to make it work again. _Configuration -> Integrations -> {Integration} -> Options_
| Fields name | Type | Required | Default | Description | -|-------------------|-----------|----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------| +| ----------------- | --------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Host | Textbox | + | - | Hostname or IP address to access EdgeOS device, can hold also port (HOST:PORT), default port is 443 | | Username | Textbox | + | - | Username of user with `Operator` level access or higher, better to create a dedicated user for that integration for faster issues identification | | Password | Textbox | + | - | | @@ -78,8 +79,9 @@ logger: ## Components ### System + | Entity Name | Type | Description | Additional information | -|-------------------------------------|---------------|---------------------------------------------------------------------------|-----------------------------------------------| +| ----------------------------------- | ------------- | ------------------------------------------------------------------------- | --------------------------------------------- | | {Router Name} Unit | Select | Sets whether to monitor device and create all the components below or not | | | {Router Name} CPU | Sensor | Represents CPU usage | | | {Router Name} RAM | Sensor | Represents RAM usage | | @@ -88,11 +90,12 @@ logger: | {Router Name} Firmware Updates | Binary Sensor | New firmware available indication | Attributes holds the url and new release name | | {Router Name} Log incoming messages | Switch | Sets whether to log WebSocket incoming messages for debugging | | -*Changing the unit will reload the integration* +_Changing the unit will reload the integration_ ### Per device + | Entity Name | Type | Description | Additional information | -|----------------------------------------------|----------------|---------------------------------------------------------------------------------|-----------------------------| +| -------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | --------------------------- | | {Router Name} {Device Name} Monitored | Sensor | Sets whether to monitor device and create all the components below or not | | | {Router Name} {Device Name} Received Rate | Sensor | Received Rate per second | Statistics: Measurement | | {Router Name} {Device Name} Received Traffic | Sensor | Received total traffic | Statistics: Total Increment | @@ -100,10 +103,10 @@ logger: | {Router Name} {Device Name} Sent Traffic | Sensor | Sent total traffic | Statistics: Total Increment | | {Router Name} {Device Name} | Device Tracker | Indication whether the device is or was connected over the configured timeframe | | - ### Per interface + | Entity Name | Type | Description | Additional information | -|---------------------------------------------------------|---------------|------------------------------------------------------------------------------|---------------------------------------------| +| ------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------- | ------------------------------------------- | | {Router Name} {Interface Name} Status | Switch | Sets whether to interface is active or not | Available only if user level is `admin` | | {Router Name} {Interface Name} Status | Binary Sensor | Indicates whether interface is active or not | Available only if user level is not `admin` | | {Router Name} {Interface Name} Connected | Binary Sensor | Indicates whether interface's port is connected or not | | @@ -119,13 +122,14 @@ logger: | {Router Name} {Interface Name} Sent Errors | Sensor | Sent errors | Statistics: Total Increment | | {Router Name} {Interface Name} Sent Packets | Sensor | Sent packets | Statistics: Total Increment | - _Unit of measurement for `Traffic` and `Rate` are according to the unit settings of the integration_ ## Services ### Update configuration + Allows to set: + - Consider away interval - Time to consider a device without activity as AWAY (any value between 10 and 1800 in seconds) - Log incoming messages - Enable / Disable logging of incoming WebSocket messages for debug - Store debug data - Enable / Disable store debug data to './storage' directory of HA for API (edgeos.debug.api.json) and WS (edgeos.debug.ws.json) data for faster debugging or just to get more ideas for additional features @@ -138,7 +142,7 @@ More details available in `Developer tools` -> `Services` -> `edgeos.update_conf ```yaml service: edgeos.update_configuration data: - device_id: {Main device ID} + device_id: { Main device ID } unit: Bytes log_incoming_messages: true consider_away_interval: 180 @@ -146,4 +150,4 @@ data: update_entities_interval: 1 ``` -*Changing the unit will reload the integration* +_Changing the unit will reload the integration_ diff --git a/pyproject.toml b/pyproject.toml index f478815..23ee312 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,154 @@ [tool.black] -target-version = ["py39"] +target-version = ["py39", "py310"] +extend-exclude = "/generated/" + +[tool.isort] +# https://github.com/PyCQA/isort/wiki/isort-Settings +profile = "black" +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +known_first_party = [ + "homeassistant", + "tests", +] +forced_separate = [ + "tests", +] +combine_as_imports = true + +[tool.pylint.MAIN] +py-version = "3.9" +ignore = [ + "tests", +] +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs = 2 +init-hook = """\ + from pathlib import Path; \ + import sys; \ + from pylint.config import find_default_config_files; \ + sys.path.append( \ + str(Path(next(find_default_config_files())).parent.joinpath('pylint/plugins')) + ) \ + """ +load-plugins = [ + "pylint.extensions.code_style", + "pylint.extensions.typing", + "hass_constructor", + "hass_enforce_type_hints", + "hass_imports", + "hass_logger", +] +persistent = false +extension-pkg-allow-list = [ + "av.audio.stream", + "av.stream", + "ciso8601", + "orjson", + "cv2", +] +fail-on = [ + "I", +] + +[tool.pylint.BASIC] +class-const-naming-style = "any" +good-names = [ + "_", + "ev", + "ex", + "fp", + "i", + "id", + "j", + "k", + "Run", + "ip", +] + +[tool.pylint."MESSAGES CONTROL"] +# Reasons disabled: +# format - handled by black +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this +# consider-using-f-string - str.format sometimes more readable +# --- +# Enable once current issues are fixed: +# consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension) +# consider-using-assignment-expr (Pylint CodeStyle extension) +disable = [ + "format", + "abstract-method", + "cyclic-import", + "duplicate-code", + "inconsistent-return-statements", + "locally-disabled", + "not-context-manager", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-branches", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-return-statements", + "too-many-statements", + "too-many-boolean-expressions", + "unused-argument", + "wrong-import-order", + "consider-using-f-string", + "consider-using-namedtuple-or-dataclass", + "consider-using-assignment-expr", +] +enable = [ + #"useless-suppression", # temporarily every now and then to clean them up + "use-symbolic-message-instead", +] + +[tool.pylint.REPORTS] +score = false + +[tool.pylint.TYPECHECK] +ignored-classes = [ + "_CountingAttr", # for attrs +] +mixin-class-rgx = ".*[Mm]ix[Ii]n" + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.pylint.EXCEPTIONS] +overgeneral-exceptions = [ + "BaseException", + "Exception", + "HomeAssistantError", +] + +[tool.pylint.TYPING] +runtime-typing = false + +[tool.pylint.CODE_STYLE] +max-line-length-suggestions = 72 + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] +norecursedirs = [ + ".git", + "testing_config", +] +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" diff --git a/setup.cfg b/setup.cfg index 1620535..781d7d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,40 +1,17 @@ [flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +exclude = .venv,.git,docs,venv,bin,lib,deps,build +max-complexity = 25 doctests = True # To work with Black -max-line-length = 88 # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring # W504 line break after binary operator -# F405 -# F403 ignore = E501, W503, E203, D202, - W504, - F405, - F403, - -[isort] -# https://github.com/timothycrosley/isort -# https://github.com/timothycrosley/isort/wiki/isort-Settings -# splits long import on multiple lines indented by 4 spaces -multi_line_output = 3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 -indent = " " -# by default isort don't check module indexes -not_skip = __init__.py -# will group `import x` and `from x import` of the same module. -force_sort_within_sections = true -sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -default_section = THIRDPARTY -known_first_party = homeassistant,tests -forced_separate = tests -combine_as_imports = true + W504 +noqa-require-code = True