From ec9102296d3635b4bdc3e2c6a139646f0d25bad0 Mon Sep 17 00:00:00 2001 From: Benjamin Segall Date: Sun, 13 Aug 2023 02:06:49 +0000 Subject: [PATCH] Add diagnostics --- .vscode/settings.json | 9 ++- custom_components/generac/__init__.py | 4 +- custom_components/generac/diagnostics.py | 84 ++++++++++++++++++++ custom_components/generac/entity.py | 7 +- custom_components/generac/image.py | 5 ++ custom_components/generac/models.py | 97 ++++++++++++------------ 6 files changed, 151 insertions(+), 55 deletions(-) create mode 100644 custom_components/generac/diagnostics.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e42720..112cf93 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,9 +5,10 @@ "*.yaml": "home-assistant" }, "[python]": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.defaultFormatter": "ms-python.black-formatter" }, - "python.formatting.provider": "black", + "python.formatting.provider": "none", "python.linting.flake8Enabled": true, "[json]": { "editor.quickSuggestions": { @@ -16,5 +17,7 @@ "editor.suggest.insertMode": "replace", "editor.formatOnSave": false }, - "json.format.enable": false + "json.format.enable": false, + + "python.analysis.typeCheckingMode": "basic" } diff --git a/custom_components/generac/__init__.py b/custom_components/generac/__init__.py index 27277c6..0e611cf 100644 --- a/custom_components/generac/__init__.py +++ b/custom_components/generac/__init__.py @@ -38,8 +38,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) _LOGGER.info(STARTUP_MESSAGE) - username = entry.data.get(CONF_USERNAME) - password = entry.data.get(CONF_PASSWORD) + username = entry.data.get(CONF_USERNAME, "") + password = entry.data.get(CONF_PASSWORD, "") session = async_get_clientsession(hass) client = GeneracApiClient(username, password, session) diff --git a/custom_components/generac/diagnostics.py b/custom_components/generac/diagnostics.py new file mode 100644 index 0000000..c80805c --- /dev/null +++ b/custom_components/generac/diagnostics.py @@ -0,0 +1,84 @@ +"""Diagnostics support for Generac.""" +from __future__ import annotations + +import ipaddress +from dataclasses import asdict +from email.utils import parseaddr +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import GeneracDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: GeneracDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + diagnostics_data = { + "data": redact( + {gen_id: asdict(item) for gen_id, item in coordinator.data.items()}, False + ), + } + + return diagnostics_data + + +DataDict = dict[str, "str | DataDict"] + + +def redact(data: Any, redact_all: bool): + if isinstance(data, dict): + return redact_dict(data, redact_all) + if isinstance(data, list): + return redact_array(data, redact_all) + if isinstance(data, str): + if parseaddr(data) == ("", ""): + return "REDACTED_VALID_EMAIL" + if is_ipv4(data): + return "REDACTED_IPV4" + if is_ipv6(data): + return "REDACTED_IPV4" + if redact_all: + return "REDACTED" + return data + + +def is_ipv4(s: str): + try: + ipaddress.IPv4Network(s) + return True + except ValueError: + return False + + +def is_ipv6(s: str): + try: + ipaddress.IPv4Network(s) + return True + except ValueError: + return False + + +_REDACTED_KEYS = { + "deviceSsid", + "serialNumber", + "apparatusId", + "address", + "localizedAddress", +} + + +def redact_dict(data: dict, redact_all: bool) -> dict: + return { + k: redact(v, True) if k in _REDACTED_KEYS else redact(v, redact_all) + for k, v in data.items() + } + + +def redact_array(data: list, redact_all: bool) -> list: + return [redact(it, redact_all) for it in data] diff --git a/custom_components/generac/entity.py b/custom_components/generac/entity.py index a89657a..6fe5db6 100644 --- a/custom_components/generac/entity.py +++ b/custom_components/generac/entity.py @@ -16,6 +16,9 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) +_EMPTY_ITEM = Item(apparatus=Apparatus(), apparatusDetail=ApparatusDetail(), empty=True) + + class GeneracEntity(CoordinatorEntity[GeneracDataUpdateCoordinator]): def __init__( self, @@ -55,7 +58,7 @@ def device_state_attributes(self): @property def available(self): """Return True if entity is available.""" - return self.coordinator.is_online + return self.coordinator.is_online and not self.item.empty async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" @@ -75,6 +78,6 @@ def aparatus_detail(self) -> ApparatusDetail: @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self.item = self.coordinator.data.get(self.generator_id) + self.item = self.coordinator.data.get(self.generator_id, _EMPTY_ITEM) _LOGGER.debug(f"Updated data for {self.unique_id}: {self.item}") self.async_write_ha_state() diff --git a/custom_components/generac/image.py b/custom_components/generac/image.py index 54a773f..dc97121 100644 --- a/custom_components/generac/image.py +++ b/custom_components/generac/image.py @@ -47,3 +47,8 @@ def name(self): @property def image_url(self): return self.aparatus_detail.heroImageUrl + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self.aparatus_detail.heroImageUrl is not None diff --git a/custom_components/generac/models.py b/custom_components/generac/models.py index 5caa9cd..4e4bde3 100644 --- a/custom_components/generac/models.py +++ b/custom_components/generac/models.py @@ -80,25 +80,25 @@ class Temperature: @dataclass() class Apparatus: - apparatusId: Optional[int] - serialNumber: Optional[str] - name: Optional[str] - type: Optional[int] - localizedAddress: Optional[str] - materialDescription: Optional[str] - heroImageUrl: Optional[str] - apparatusStatus: Optional[int] - isConnected: Optional[bool] - isConnecting: Optional[bool] - showWarning: Optional[bool] - weather: Optional[Weather] - preferredDealerName: Optional[str] - preferredDealerPhone: Optional[str] - preferredDealerEmail: Optional[str] - isDealerManaged: Optional[bool] - isDealerUnmonitored: Optional[bool] - modelNumber: Optional[str] - panelId: Optional[str] + apparatusId: Optional[int] = None + serialNumber: Optional[str] = None + name: Optional[str] = None + type: Optional[int] = None + localizedAddress: Optional[str] = None + materialDescription: Optional[str] = None + heroImageUrl: Optional[str] = None + apparatusStatus: Optional[int] = None + isConnected: Optional[bool] = None + isConnecting: Optional[bool] = None + showWarning: Optional[bool] = None + weather: Optional[Weather] = None + preferredDealerName: Optional[str] = None + preferredDealerPhone: Optional[str] = None + preferredDealerEmail: Optional[str] = None + isDealerManaged: Optional[bool] = None + isDealerUnmonitored: Optional[bool] = None + modelNumber: Optional[str] = None + panelId: Optional[str] = None @dataclass class Property: @@ -118,7 +118,7 @@ class Value: value: Optional[Value | list] type: Optional[int] - properties: Optional[list[Property]] + properties: Optional[list[Property]] = None @dataclass @@ -157,37 +157,38 @@ class ProductInfo: value: Optional[str] type: Optional[int] - apparatusId: Optional[int] - name: Optional[str] - serialNumber: Optional[str] - apparatusClassification: Optional[int] - panelId: Optional[str] - activationDate: Optional[str] - deviceType: Optional[str] - deviceSsid: Optional[str] - shortDeviceId: Optional[str] - apparatusStatus: Optional[int] - heroImageUrl: Optional[str] - statusLabel: Optional[str] - statusText: Optional[str] - eCodeLabel: Optional[str] - weather: Optional[Weather] - isConnected: Optional[bool] - isConnecting: Optional[bool] - showWarning: Optional[bool] - hasMaintenanceAlert: Optional[bool] - lastSeen: Optional[str] - connectionTimestamp: Optional[str] - address: Optional[Address] - properties: Optional[list[Property]] - subscription: Optional[Subscription] - enrolledInVpp: Optional[bool] - hasActiveVppEvent: Optional[bool] - productInfo: Optional[list[Property]] - hasDisconnectedNotificationsOn: Optional[bool] + apparatusId: Optional[int] = None + name: Optional[str] = None + serialNumber: Optional[str] = None + apparatusClassification: Optional[int] = None + panelId: Optional[str] = None + activationDate: Optional[str] = None + deviceType: Optional[str] = None + deviceSsid: Optional[str] = None + shortDeviceId: Optional[str] = None + apparatusStatus: Optional[int] = None + heroImageUrl: Optional[str] = None + statusLabel: Optional[str] = None + statusText: Optional[str] = None + eCodeLabel: Optional[str] = None + weather: Optional[Weather] = None + isConnected: Optional[bool] = None + isConnecting: Optional[bool] = None + showWarning: Optional[bool] = None + hasMaintenanceAlert: Optional[bool] = None + lastSeen: Optional[str] = None + connectionTimestamp: Optional[str] = None + address: Optional[Address] = None + properties: Optional[list[Property]] = None + subscription: Optional[Subscription] = None + enrolledInVpp: Optional[bool] = None + hasActiveVppEvent: Optional[bool] = None + productInfo: Optional[list[Property]] = None + hasDisconnectedNotificationsOn: Optional[bool] = None @dataclass class Item: apparatus: Apparatus apparatusDetail: ApparatusDetail + empty: bool = False