Coverage for custom_components/remote_logger/sensor.py: 98%
43 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-07 04:46 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-07 04:46 +0000
1"""Binary sensor platform for remote_logger."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import TYPE_CHECKING
8from homeassistant.components.sensor import SensorEntity, SensorEntityDescription, SensorStateClass
9from homeassistant.helpers import device_registry as dr
10from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
11from homeassistant.util import slugify
13from custom_components.remote_logger.exporter import LogExporter
15from .const import DOMAIN
16from .remote_logger import REF_EXPORTER
18if TYPE_CHECKING:
19 from collections.abc import Callable
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.core import HomeAssistant
23 from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
25 from custom_components.remote_logger.exporter import LogExporter
27from typing import TYPE_CHECKING, Any
29from homeassistant.const import EntityCategory
32@dataclass(frozen=True, kw_only=True)
33class RemoteLoggerDiagnosticEntityDescription(SensorEntityDescription):
34 """Describes diagnostic sensor entity."""
36 value_fn: Callable[[LogExporter], str | int | float | None]
37 attr_fn: Callable[[LogExporter], dict[str, Any]] = lambda _: {}
40SENSORS: tuple[RemoteLoggerDiagnosticEntityDescription, ...] = (
41 RemoteLoggerDiagnosticEntityDescription(
42 key="format_errors",
43 translation_key="format_errors",
44 native_unit_of_measurement="error",
45 entity_category=EntityCategory.DIAGNOSTIC,
46 state_class=SensorStateClass.MEASUREMENT,
47 value_fn=lambda logger: logger.format_error_count,
48 attr_fn=lambda exporter: {
49 "last_error_time": exporter.last_format_error,
50 "last_error_message": exporter.last_format_error_message,
51 },
52 ),
53 RemoteLoggerDiagnosticEntityDescription(
54 key="posting_errors",
55 translation_key="posting_errors",
56 native_unit_of_measurement="error",
57 entity_category=EntityCategory.DIAGNOSTIC,
58 state_class=SensorStateClass.MEASUREMENT,
59 value_fn=lambda logger: logger.posting_error_count,
60 attr_fn=lambda exporter: {
61 "last_error_time": exporter.last_posting_error,
62 "last_error_message": exporter.last_posting_error_message,
63 },
64 ),
65 RemoteLoggerDiagnosticEntityDescription(
66 key="events",
67 translation_key="events",
68 native_unit_of_measurement="event",
69 entity_category=EntityCategory.DIAGNOSTIC,
70 state_class=SensorStateClass.MEASUREMENT,
71 value_fn=lambda exporter: exporter.event_count,
72 attr_fn=lambda exporter: {"last_event_time": exporter.last_event},
73 ),
74 RemoteLoggerDiagnosticEntityDescription(
75 key="postings",
76 translation_key="postings",
77 native_unit_of_measurement="posting",
78 entity_category=EntityCategory.DIAGNOSTIC,
79 state_class=SensorStateClass.MEASUREMENT,
80 value_fn=lambda exporter: exporter.posting_count,
81 attr_fn=lambda exporter: {"last_posting_time": exporter.last_posting},
82 ),
83)
86class LoggerEntity(SensorEntity):
87 """Represent a diagnostic tracking logger."""
89 _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC # pyright: ignore[reportIncompatibleVariableOverride]
90 _attr_should_poll = True
91 _attr_has_entity_name = True
93 def __init__(
94 self, exporter: LogExporter, description: RemoteLoggerDiagnosticEntityDescription, device_info: DeviceInfo
95 ) -> None:
96 super().__init__()
97 self._exporter: LogExporter = exporter
98 self.entity_description: RemoteLoggerDiagnosticEntityDescription = description # pyright: ignore[reportIncompatibleVariableOverride]
99 self._attr_unique_id = slugify(f"{exporter.name}_{description.key}")
100 self._attr_device_info = device_info
101 self._attr_translation_key = description.translation_key
103 @property
104 def native_value(self) -> str | int | float | None: # pyright: ignore[reportIncompatibleVariableOverride]
105 """Return the state."""
106 return self.entity_description.value_fn(self._exporter)
108 @property
109 def extra_state_attributes(self) -> dict[str, Any]: # pyright: ignore[reportIncompatibleVariableOverride]
110 """Return the state attributes."""
111 return self.entity_description.attr_fn(self._exporter)
114async def async_setup_entry(
115 hass: HomeAssistant,
116 entry: ConfigEntry,
117 async_add_entities: AddConfigEntryEntitiesCallback,
118) -> None:
119 """Set up remote_logger binary sensor from a config entry."""
120 exporter: LogExporter = hass.data[DOMAIN][entry.entry_id][REF_EXPORTER]
121 device_info = DeviceInfo(
122 entry_type=DeviceEntryType.SERVICE,
123 identifiers={(DOMAIN, entry.entry_id)},
124 manufacturer="Rhizomatics",
125 name=f"{exporter.name} Remote Logger",
126 )
127 async_add_entities(LoggerEntity(exporter, description, device_info) for description in SENSORS)
129 # Remove any disabled duplicate devices for this config entry that are not
130 # the canonical one (identified by entry.entry_id). These can accumulate
131 # when the integration previously used endpoint-derived identifiers.
132 registry: dr.DeviceRegistry = dr.async_get(hass)
133 canonical_id: tuple[str, str] = (DOMAIN, entry.entry_id)
134 for device in dr.async_entries_for_config_entry(registry, entry.entry_id):
135 if canonical_id not in device.identifiers and device.disabled:
136 registry.async_remove_device(device.id)