diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ffdb7..903538c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 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 | +| /api/edgeos/{ENTRY_ID}/ws | GET | JSON of all raw data from the EdgeOS WebSocket, per integration | + +**Authentication: Requires long-living token from HA** + ## 2.0.13 - Add support for all interfaces but `loopback` [#76](https://github.com/elad-bar/ha-edgeos/issues/76) diff --git a/README.md b/README.md index a6f242f..e6d17c2 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,8 @@ logger: | {Router Name} RAM | Sensor | Represents RAM usage | | | {Router Name} Uptime | Sensor | Represents last time the EdgeOS was restarted | | | {Router Name} Unknown devices | Sensor | Represents number of devices leased by the DHCP server | Attributes holds the leased hostname and IPs | -| {Router Name} Received Messages | Sensor | Represents the number of WS messages handled | | -| {Router Name} Ignored Messages | Sensor | Represents the number of WS messages ignored | | -| {Router Name} Error Messages | Sensor | Represents the number of WS messages of errors | | | {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 | | -| {Router Name} Store Debug Data | Switch | Sets whether to store API and WebSocket latest data for debugging | | *Changing the unit will reload the integration* @@ -150,3 +146,14 @@ data: ``` *Changing the unit will reload the integration* + +## Endpoints + +| 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, per integration | +| /api/edgeos/{ENTRY_ID}/api | GET | JSON of all raw data from the EdgeOS API, per integration | +| /api/edgeos/{ENTRY_ID}/ws | GET | JSON of all raw data from the EdgeOS WebSocket, per integration | + +**Authentication: Requires long-living token from HA** diff --git a/custom_components/edgeos/component/api/storage_api.py b/custom_components/edgeos/component/api/storage_api.py index b3aeb6a..bf6367d 100644 --- a/custom_components/edgeos/component/api/storage_api.py +++ b/custom_components/edgeos/component/api/storage_api.py @@ -1,9 +1,8 @@ """Storage handlers.""" from __future__ import annotations -from datetime import datetime -import json import logging +import sys from typing import Awaitable, Callable from homeassistant.core import HomeAssistant @@ -14,12 +13,16 @@ from ...core.api.base_api import BaseAPI from ...core.helpers.enums import ConnectivityStatus from ..helpers.const import * +from ..models.base_view import EdgeOSBaseView _LOGGER = logging.getLogger(__name__) class StorageAPI(BaseAPI): - _storage: Store + _stores: dict[str, Store] | None + _views: dict[str, EdgeOSBaseView] | None + _config_data: ConfigData | None + _data: dict def __init__(self, hass: HomeAssistant, @@ -29,29 +32,14 @@ def __init__(self, super().__init__(hass, async_on_data_changed, async_on_status_changed) - self._storages = None + self._config_data = None + self._stores = None + self._views = None + self._data = {} @property def _storage_config(self) -> Store: - storage = self._storages.get(STORAGE_DATA_FILE_CONFIG) - - return storage - - @property - def _storage_api(self) -> Store: - storage = self._storages.get(STORAGE_DATA_FILE_API_DEBUG) - - return storage - - @property - def _storage_ws(self) -> Store: - storage = self._storages.get(STORAGE_DATA_FILE_WS_DEBUG) - - return storage - - @property - def _storage_ha(self) -> Store: - storage = self._storages.get(STORAGE_DATA_FILE_HA_DEBUG) + storage = self._stores.get(STORAGE_DATA_FILE_CONFIG) return storage @@ -104,17 +92,52 @@ def update_api_interval(self): return result async def initialize(self, config_data: ConfigData): - storages = {} - entry_id = config_data.entry.entry_id + self._config_data = config_data + + self._initialize_routes() + self._initialize_storages() + + await self._async_load_configuration() + + def _initialize_storages(self): + stores = {} + + entry_id = self._config_data.entry.entry_id for storage_data_file in STORAGE_DATA_FILES: file_name = f"{DOMAIN}.{entry_id}.{storage_data_file}.json" - storages[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._storages = storages + self._stores = stores - await self._async_load_configuration() + def _initialize_routes(self): + try: + main_view_data = {} + entry_id = self._config_data.entry.entry_id + + for key in STORAGE_API_DATA: + view = EdgeOSBaseView(self.hass, key, self._get_data, entry_id) + + main_view_data[key] = view.url + + self.hass.http.register_view(view) + + main_view = self.hass.data.get(MAIN_VIEW) + + if main_view is None: + main_view = EdgeOSBaseView(self.hass, STORAGE_API_LIST, self._get_data) + + self.hass.http.register_view(main_view) + self.hass.data[MAIN_VIEW] = main_view + + self._data[STORAGE_API_LIST] = main_view_data + + 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}") async def _async_load_configuration(self): """Load the retained data from store and return de-serialized data.""" @@ -175,13 +198,6 @@ async def set_log_incoming_messages(self, enabled: bool): await self._async_save() - async def set_store_debug_data(self, enabled: bool): - _LOGGER.debug(f"Set store debug data to {enabled}") - - self.data[STORAGE_DATA_STORE_DEBUG_DATA] = enabled - - await self._async_save() - async def set_consider_away_interval(self, interval: int): _LOGGER.debug(f"Changing {STORAGE_DATA_CONSIDER_AWAY_INTERVAL}: {interval}") @@ -204,43 +220,51 @@ async def set_update_api_interval(self, interval: int): await self._async_save() async def debug_log_api(self, data: dict): - if self.store_debug_data and data is not None: - await self._storage_api.async_save(self._get_json_data(data)) + self._data[STORAGE_API_DATA_API] = data async def debug_log_ws(self, data: dict): - if self.store_debug_data and data is not None: - await self._storage_ws.async_save(self._get_json_data(data)) + self._data[STORAGE_API_DATA_WS] = data async def debug_log_ha(self, data: dict): - if self.store_debug_data and data is not None: - clean_data = {} - for key in data: - if key in [DEVICE_LIST, API_DATA_INTERFACES]: - new_item = {} - items = data.get(key, {}) + clean_data = {} + for key in data: + if key in [DEVICE_LIST, API_DATA_INTERFACES]: + new_item = {} + items = data.get(key, {}) - for item_key in items: - item = items.get(item_key) - new_item[item_key] = item.to_dict() + for item_key in items: + item = items.get(item_key) + new_item[item_key] = item.to_dict() - clean_data[key] = new_item + clean_data[key] = new_item - elif key in [API_DATA_SYSTEM]: - item = data.get(key) - clean_data[key] = item.to_dict() + elif key in [API_DATA_SYSTEM]: + item = data.get(key) + clean_data[key] = item.to_dict() - await self._storage_ha.async_save(self._get_json_data(clean_data)) + else: + clean_data[key] = data.get(key) - def _get_json_data(self, data: dict): - json_data = json.dumps(data, default=self.json_converter, sort_keys=True, indent=4) + self._data[STORAGE_API_DATA_HA] = clean_data - result = json.loads(json_data) + def _get_data(self, key): + is_list = key == STORAGE_API_LIST - return result + data = {} if is_list else self._data.get(key) + + if is_list: + raw_data = self._data.get(key) + current_entry_id = self._config_data.entry.entry_id + + for entry_id in self.hass.data[DATA].keys(): + entry_data = {} + + for raw_data_key in raw_data: + url_raw = raw_data.get(raw_data_key) + url = url_raw.replace(current_entry_id, entry_id) + + entry_data[raw_data_key] = url + + data[entry_id] = entry_data - @staticmethod - def json_converter(data): - if isinstance(data, datetime): - return data.__str__() - if isinstance(data, dict): - return data.__dict__ + return data diff --git a/custom_components/edgeos/component/helpers/const.py b/custom_components/edgeos/component/helpers/const.py index 4f4746d..abc177f 100644 --- a/custom_components/edgeos/component/helpers/const.py +++ b/custom_components/edgeos/component/helpers/const.py @@ -27,17 +27,23 @@ DEFAULT_CONSIDER_AWAY_INTERVAL = timedelta(minutes=3) STORAGE_DATA_FILE_CONFIG = "config" -STORAGE_DATA_FILE_API_DEBUG = "debug.api" -STORAGE_DATA_FILE_WS_DEBUG = "debug.ws" -STORAGE_DATA_FILE_HA_DEBUG = "debug.ha" +STORAGE_API_LIST = "list" +STORAGE_API_DATA_API = "api" +STORAGE_API_DATA_WS = "ws" +STORAGE_API_DATA_HA = "ha" STORAGE_DATA_FILES = [ - STORAGE_DATA_FILE_CONFIG, - STORAGE_DATA_FILE_API_DEBUG, - STORAGE_DATA_FILE_WS_DEBUG, - STORAGE_DATA_FILE_HA_DEBUG + STORAGE_DATA_FILE_CONFIG ] +STORAGE_API_DATA = [ + STORAGE_API_DATA_API, + STORAGE_API_DATA_WS, + STORAGE_API_DATA_HA +] + +MESSAGES_COUNTER_SECTION = "messages" + STORAGE_DATA_MONITORED_INTERFACES = "monitored-interfaces" STORAGE_DATA_MONITORED_DEVICES = "monitored-devices" STORAGE_DATA_UNIT = "unit" @@ -106,6 +112,12 @@ WS_IGNORED_MESSAGES = "ignored-messages" WS_ERROR_MESSAGES = "error-messages" +WS_MESSAGES = [ + WS_RECEIVED_MESSAGES, + WS_IGNORED_MESSAGES, + WS_ERROR_MESSAGES +] + UPDATE_DATE_ENDPOINTS = [ API_DATA_SYS_INFO, API_DATA_DHCP_STATS, diff --git a/custom_components/edgeos/component/managers/home_assistant.py b/custom_components/edgeos/component/managers/home_assistant.py index e838c49..aa95549 100644 --- a/custom_components/edgeos/component/managers/home_assistant.py +++ b/custom_components/edgeos/component/managers/home_assistant.py @@ -13,7 +13,6 @@ BinarySensorDeviceClass, BinarySensorEntityDescription, ) - from homeassistant.components.sensor import SensorEntityDescription, SensorStateClass from homeassistant.components.switch import SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -230,11 +229,6 @@ def load_entities(self): self._load_uptime_sensor() self._load_firmware_upgrade_binary_sensor() self._load_log_incoming_messages_switch() - self._load_store_debug_data_switch() - - self._load_received_messages_sensor() - self._load_ignored_messages_sensor() - self._load_error_messages_sensor() for unique_id in self._devices: device_item = self._get_device(unique_id) @@ -348,10 +342,19 @@ async def _extract_api_data(self): _LOGGER.error(f"Failed to extract API data, Error: {ex}, Line: {line_number}") async def _log_ha_data(self): + messages = {} + + for key in WS_MESSAGES: + message_counter = self._ws.data.get(key, 0) + counter_name = key.replace(f"-{MESSAGES_COUNTER_SECTION.lower()}", "") + + messages[counter_name] = message_counter + data = { API_DATA_SYSTEM: self._system, DEVICE_LIST: self._devices, - API_DATA_INTERFACES: self._interfaces + API_DATA_INTERFACES: self._interfaces, + MESSAGES_COUNTER_SECTION: messages } await self.storage_api.debug_log_ha(data) @@ -849,105 +852,6 @@ def _load_uptime_sensor(self): ex, f"Failed to load sensor for {entity_name}" ) - def _load_received_messages_sensor(self): - device_name = self.system_name - entity_name = f"{device_name} Received Messages" - - try: - state = self._ws.data.get(WS_RECEIVED_MESSAGES, 0) - - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } - - unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) - icon = "mdi:message-bulleted" - - entity_description = SensorEntityDescription( - key=unique_id, - name=entity_name, - icon=icon, - state_class=SensorStateClass.MEASUREMENT - ) - - 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}" - ) - - def _load_ignored_messages_sensor(self): - device_name = self.system_name - entity_name = f"{device_name} Ignored Messages" - - try: - state = self._ws.data.get(WS_IGNORED_MESSAGES, 0) - - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } - - unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) - icon = "mdi:message-bulleted-off" - - entity_description = SensorEntityDescription( - key=unique_id, - name=entity_name, - icon=icon, - state_class=SensorStateClass.MEASUREMENT - ) - - 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}" - ) - - def _load_error_messages_sensor(self): - device_name = self.system_name - entity_name = f"{device_name} Error Messages" - - try: - state = self._ws.data.get(WS_ERROR_MESSAGES, 0) - - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } - - unique_id = EntityData.generate_unique_id(DOMAIN_SENSOR, entity_name) - icon = "mdi:message-alert" - - entity_description = SensorEntityDescription( - key=unique_id, - name=entity_name, - icon=icon, - state_class=SensorStateClass.MEASUREMENT - ) - - 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}" - ) - def _load_firmware_upgrade_binary_sensor(self): device_name = self.system_name entity_name = f"{device_name} Firmware Upgrade" @@ -1018,43 +922,6 @@ def _load_log_incoming_messages_switch(self): ex, f"Failed to load log incoming messages switch for {entity_name}" ) - def _load_store_debug_data_switch(self): - device_name = self.system_name - entity_name = f"{device_name} Store Debug Data" - - try: - state = self.storage_api.store_debug_data - - attributes = { - ATTR_FRIENDLY_NAME: entity_name - } - - unique_id = EntityData.generate_unique_id(DOMAIN_SWITCH, entity_name) - - icon = "mdi:file-download" - - entity_description = SwitchEntityDescription( - key=unique_id, - name=entity_name, - icon=icon, - entity_category=EntityCategory.CONFIG - ) - - 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_store_debug_data) - self.set_action(unique_id, ACTION_CORE_ENTITY_TURN_OFF, self._disable_store_debug_data) - - except Exception as ex: - self.log_exception( - ex, f"Failed to load store debug data switch for {entity_name}" - ) - def _load_device_received_rate_sensor(self, device: EdgeOSDeviceData): unit_of_measurement = self._get_rate_unit_of_measurement() @@ -1480,12 +1347,6 @@ async def _enable_log_incoming_messages(self, entity: EntityData): async def _disable_log_incoming_messages(self, entity: EntityData): await self.storage_api.set_log_incoming_messages(False) - async def _enable_store_debug_data(self, entity: EntityData): - await self.storage_api.set_store_debug_data(True) - - async def _disable_store_debug_data(self, entity: EntityData): - await self.storage_api.set_store_debug_data(False) - async def _set_unit(self, entity: EntityData, option: str): await self.storage_api.set_unit(option) @@ -1575,7 +1436,6 @@ async def _update_configuration_data(self, data: dict): 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_STORE_DEBUG_DATA: self.storage_api.set_store_debug_data, STORAGE_DATA_UNIT: self.storage_api.set_unit } diff --git a/custom_components/edgeos/component/models/base_view.py b/custom_components/edgeos/component/models/base_view.py new file mode 100644 index 0000000..59024bd --- /dev/null +++ b/custom_components/edgeos/component/models/base_view.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import logging +from typing import Callable + +from homeassistant.components.http import HomeAssistantView + +from ..helpers.const import * + +_LOGGER = logging.getLogger(__name__) + + +class EdgeOSBaseView(HomeAssistantView): + name: str + _prefix: str + _get_data_callback: Callable[[str], dict] + + def __init__(self, hass, prefix: str, get_data_callback: Callable[[str], dict], entry_id: str | None = None): + self.data = None + self._entry_id = entry_id + self._hass = hass + self._get_data_callback = get_data_callback + self._prefix = prefix + + if entry_id is None: + self.url = f"/api/{DOMAIN}/{prefix}" + + else: + self.url = f"/api/{DOMAIN}/{entry_id}/{prefix}" + + self.name = self.url.replace("/", ":") + + async def get(self, request): + return self.json(self._get_data_callback(self._prefix)) diff --git a/custom_components/edgeos/configuration/helpers/const.py b/custom_components/edgeos/configuration/helpers/const.py index 5363e56..8b893b7 100644 --- a/custom_components/edgeos/configuration/helpers/const.py +++ b/custom_components/edgeos/configuration/helpers/const.py @@ -11,7 +11,7 @@ DEFAULT_NAME = "EdgeOS" MANUFACTURER = "Ubiquiti" -CONFIGURATION_MANAGER = f"cm_{DOMAIN}" +MAIN_VIEW = f"main_view_{DOMAIN}" DATA_KEYS = [ CONF_HOST, diff --git a/custom_components/edgeos/manifest.json b/custom_components/edgeos/manifest.json index 7a03271..f5d78de 100644 --- a/custom_components/edgeos/manifest.json +++ b/custom_components/edgeos/manifest.json @@ -7,6 +7,6 @@ "codeowners": ["@elad-bar"], "requirements": ["aiohttp"], "config_flow": true, - "version": "2.0.13", + "version": "2.0.14", "iot_class": "local_polling" } diff --git a/info.md b/info.md index a6f242f..e6d17c2 100644 --- a/info.md +++ b/info.md @@ -85,12 +85,8 @@ logger: | {Router Name} RAM | Sensor | Represents RAM usage | | | {Router Name} Uptime | Sensor | Represents last time the EdgeOS was restarted | | | {Router Name} Unknown devices | Sensor | Represents number of devices leased by the DHCP server | Attributes holds the leased hostname and IPs | -| {Router Name} Received Messages | Sensor | Represents the number of WS messages handled | | -| {Router Name} Ignored Messages | Sensor | Represents the number of WS messages ignored | | -| {Router Name} Error Messages | Sensor | Represents the number of WS messages of errors | | | {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 | | -| {Router Name} Store Debug Data | Switch | Sets whether to store API and WebSocket latest data for debugging | | *Changing the unit will reload the integration* @@ -150,3 +146,14 @@ data: ``` *Changing the unit will reload the integration* + +## Endpoints + +| 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, per integration | +| /api/edgeos/{ENTRY_ID}/api | GET | JSON of all raw data from the EdgeOS API, per integration | +| /api/edgeos/{ENTRY_ID}/ws | GET | JSON of all raw data from the EdgeOS WebSocket, per integration | + +**Authentication: Requires long-living token from HA**