Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions custom_components/generac/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DEFAULT_NAME
from .const import DOMAIN
from .coordinator import GeneracDataUpdateCoordinator
from .entity import GeneracEntity
Expand Down Expand Up @@ -36,17 +35,11 @@ def sensors() -> Type[GeneracEntity]:
]


def sensor_name(self, name_label):
return f"{DEFAULT_NAME}_{self.device_id}_{name_label}"


class GeneracConnectedSensor(GeneracEntity, BinarySensorEntity):
"""generac binary_sensor class."""

@property
def name(self):
"""Return the name of the binary_sensor."""
return sensor_name(self, "is_connected")
entity_key = "is_connected"
entity_label = "Connected"

@property
def device_class(self):
Expand All @@ -62,10 +55,8 @@ def is_on(self):
class GeneracConnectingSensor(GeneracEntity, BinarySensorEntity):
"""generac binary_sensor class."""

@property
def name(self):
"""Return the name of the binary_sensor."""
return sensor_name(self, "is_connecting")
entity_key = "is_connecting"
entity_label = "Connecting"

@property
def device_class(self):
Expand All @@ -81,10 +72,8 @@ def is_on(self):
class GeneracMaintenanceAlertSensor(GeneracEntity, BinarySensorEntity):
"""generac binary_sensor class."""

@property
def name(self):
"""Return the name of the binary_sensor."""
return sensor_name(self, "has_maintenance_alert")
entity_key = "has_maintenance_alert"
entity_label = "Maintenance Alert"

@property
def device_class(self):
Expand All @@ -100,10 +89,8 @@ def is_on(self):
class GeneracWarningSensor(GeneracEntity, BinarySensorEntity):
"""generac binary_sensor class."""

@property
def name(self):
"""Return the name of the binary_sensor."""
return sensor_name(self, "show_warning")
entity_key = "show_warning"
entity_label = "Warning"

@property
def device_class(self):
Expand Down
40 changes: 29 additions & 11 deletions custom_components/generac/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,25 @@ 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]
coordinator: GeneracDataUpdateCoordinator | None = hass.data.get(DOMAIN, {}).get(
entry.entry_id
)
if coordinator is None:
return {
"status": "not_loaded",
"data": {},
}

diagnostics_data = {
"status": "loaded",
# Keyed by position: the coordinator keys devices by apparatusId,
# which is itself redacted below.
"data": redact(
{gen_id: asdict(item) for gen_id, item in coordinator.data.items()}, False
{
f"device_{index}": asdict(item)
for index, item in enumerate((coordinator.data or {}).values())
},
False,
),
}

Expand All @@ -31,43 +45,47 @@ async def async_get_config_entry_diagnostics(
DataDict = dict[str, "str | DataDict"]


def redact(data: Any, redact_all: bool):
def redact(data: Any, redact_all: bool) -> Any:
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"
_, address = parseaddr(data)
if "@" in address:
return "REDACTED_EMAIL"
if is_ipv4(data):
return "REDACTED_IPV4"
if is_ipv6(data):
return "REDACTED_IPV4"
return "REDACTED_IPV6"
if redact_all:
return "REDACTED"
return data


def is_ipv4(s: str):
def is_ipv4(s: str) -> bool:
try:
ipaddress.IPv4Network(s)
return True
except ValueError:
except (TypeError, ValueError):
return False


def is_ipv6(s: str):
def is_ipv6(s: str) -> bool:
try:
ipaddress.IPv4Network(s)
ipaddress.IPv6Network(s)
return True
except ValueError:
except (TypeError, ValueError):
return False


_REDACTED_KEYS = {
"deviceSsid",
"serialNumber",
"apparatusId",
"panelId",
"shortDeviceId",
"deviceId",
"address",
"localizedAddress",
}
Expand Down
50 changes: 45 additions & 5 deletions custom_components/generac/entity.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""GeneracEntity class"""
"""Generac entity base classes and friendly-name helpers."""
import logging
from typing import ClassVar

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import ATTRIBUTION
from .const import DEFAULT_NAME
from .const import DOMAIN
from .coordinator import GeneracDataUpdateCoordinator
from .models import Apparatus
Expand All @@ -16,10 +18,34 @@
_LOGGER: logging.Logger = logging.getLogger(__package__)


_ENTITY_WORDS = {
"id": "ID",
"ssid": "SSID",
}


def parse_entity_name(entity_key: str) -> str:
"""Convert a stable entity key into a readable label."""
return " ".join(
_ENTITY_WORDS.get(word, word.capitalize()) for word in entity_key.split("_")
)


_EMPTY_ITEM = Item(apparatus=Apparatus(), apparatusDetail=ApparatusDetail(), empty=True)


class GeneracEntity(CoordinatorEntity[GeneracDataUpdateCoordinator]):
"""Base entity with stable registry IDs and readable display names."""

_attr_has_entity_name = True

# Stable suffix of the entity registry unique_id. Set explicitly on every
# entity class and never derive it from the class name: changing it
# orphans the user's existing entity and creates a duplicate.
entity_key: ClassVar[str]
# Display label when the one derived from entity_key reads poorly.
entity_label: ClassVar[str | None] = None

def __init__(
self,
coordinator: GeneracDataUpdateCoordinator,
Expand All @@ -35,7 +61,17 @@ def __init__(
@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return f"{self.config_entry.entry_id}_{self.device_id}_{self.name}"
# Byte-for-byte the historical format, which embedded the old
# technical name "generac_<device_id>_<key>".
return (
f"{self.config_entry.entry_id}_{self.device_id}_"
f"{DEFAULT_NAME}_{self.device_id}_{self.entity_key}"
)

@property
def name(self):
"""Return the entity label; Home Assistant prefixes the device name."""
return self.entity_label or parse_entity_name(self.entity_key)

@property
def device_info(self):
Expand All @@ -58,7 +94,11 @@ def extra_state_attributes(self):
@property
def available(self):
"""Return True if entity is available."""
return self.coordinator.is_online and not self.item.empty
return (
self.coordinator.last_update_success
and 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."""
Expand All @@ -75,6 +115,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.device_id, _EMPTY_ITEM)
_LOGGER.debug(f"Updated data for {self.unique_id}: {self.item}")
self.item = (self.coordinator.data or {}).get(self.device_id, _EMPTY_ITEM)
_LOGGER.debug("Updated Generac entity %s", self.entity_id)
self.async_write_ha_state()
8 changes: 2 additions & 6 deletions custom_components/generac/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DEFAULT_NAME
from .const import DOMAIN
from .coordinator import GeneracDataUpdateCoordinator
from .entity import GeneracEntity
Expand All @@ -31,6 +30,8 @@ async def async_setup_entry(
class HeroImageSensor(GeneracEntity, ImageEntity):
"""generac Image class."""

entity_key = "hero_image"

def __init__(
self,
coordinator: GeneracDataUpdateCoordinator,
Expand All @@ -43,11 +44,6 @@ def __init__(
super().__init__(coordinator, config_entry, device_id, item)
ImageEntity.__init__(self, hass)

@property
def name(self):
"""Return the name of the sensor."""
return f"{DEFAULT_NAME}_{self.device_id}_hero_image"

@property
def image_url(self):
return self.aparatus_detail.heroImageUrl
Expand Down
Loading
Loading