diff --git a/custom_components/localthings/sensor.py b/custom_components/localthings/sensor.py index 3b73cfc..2752758 100644 --- a/custom_components/localthings/sensor.py +++ b/custom_components/localthings/sensor.py @@ -3,9 +3,13 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorDeviceClass, SensorStateClass from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .observe import MODE_OBSERVE, MODE_POLL from .registry.entities import SensorDesc from .const import DOMAIN @@ -19,11 +23,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: coordinator: LocalThingsCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ LocalThingsSensor(coordinator, b) for b in coordinator.bound if isinstance(b.desc, SensorDesc) and _is_included(b, coordinator) - ) + ] + entities.append(LocalThingsConnectionModeSensor(coordinator)) + async_add_entities(entities) class LocalThingsSensor(LocalThingsEntity, SensorEntity): @@ -40,3 +46,29 @@ class LocalThingsSensor(LocalThingsEntity, SensorEntity): @property def native_value(self): return (self.coordinator.data or {}).get(self._state_key) + + +class LocalThingsConnectionModeSensor(CoordinatorEntity[LocalThingsCoordinator], SensorEntity): + """Diagnostic sensor exposing whether this device is currently + receiving push notifications (observe mode) or being polled only. + Disabled by default — it's for troubleshooting, not everyday use.""" + + _attr_has_entity_name = True + _attr_name = 'Connection mode' + _attr_translation_key = 'connection_mode' + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_entity_registry_enabled_default = False + _attr_device_class = SensorDeviceClass.ENUM + _attr_options = [MODE_OBSERVE, MODE_POLL] + + def __init__(self, coordinator: LocalThingsCoordinator) -> None: + super().__init__(coordinator) + self._attr_unique_id = f"{DOMAIN}_{coordinator.device_serial}_connection_mode" + + @property + def device_info(self) -> DeviceInfo: + return self.coordinator.device_info + + @property + def native_value(self) -> str: + return self.coordinator.observe_mode diff --git a/custom_components/localthings/strings.json b/custom_components/localthings/strings.json index e45696c..b181fdf 100644 --- a/custom_components/localthings/strings.json +++ b/custom_components/localthings/strings.json @@ -45,6 +45,12 @@ "icestatus_stop": "Idle", "icestatus_run": "Making ice" } + }, + "connection_mode": { + "state": { + "observe": "Push (observe)", + "poll": "Polling" + } } } }, diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index e45696c..b181fdf 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -45,6 +45,12 @@ "icestatus_stop": "Idle", "icestatus_run": "Making ice" } + }, + "connection_mode": { + "state": { + "observe": "Push (observe)", + "poll": "Polling" + } } } }, diff --git a/tests/localthings/test_sensor.py b/tests/localthings/test_sensor.py index dc8cb39..10a3ba4 100644 --- a/tests/localthings/test_sensor.py +++ b/tests/localthings/test_sensor.py @@ -6,6 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from custom_components.localthings.const import DOMAIN +from custom_components.localthings.observe import MODE_POLL async def test_sensors_registered( @@ -45,3 +46,37 @@ async def test_sensor_state_from_coordinator( assert state.state not in ('unknown', 'unavailable'), ( f"Sensor state is {state.state}" ) + + +async def test_connection_mode_sensor_registered_disabled_by_default( + hass: HomeAssistant, mock_entry, mock_coordinator_session +) -> None: + """The connection-mode diagnostic sensor is registered but disabled by + default — it's for troubleshooting, not everyday use.""" + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + ent_reg = er.async_get(hass) + entries = er.async_entries_for_config_entry(ent_reg, mock_entry.entry_id) + mode_entries = [e for e in entries if e.unique_id.endswith('_connection_mode')] + assert len(mode_entries) == 1 + entry = mode_entries[0] + assert entry.disabled_by is not None + assert entry.entity_category == 'diagnostic' + + +async def test_connection_mode_sensor_reflects_coordinator_mode( + hass: HomeAssistant, mock_entry, mock_coordinator_session +) -> None: + """native_value reads coordinator.observe_mode directly, live.""" + from custom_components.localthings.sensor import LocalThingsConnectionModeSensor + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + coordinator = hass.data[DOMAIN][mock_entry.entry_id] + sensor = LocalThingsConnectionModeSensor(coordinator) + assert sensor.native_value == MODE_POLL + + coordinator._observe._set_mode('observe') + assert sensor.native_value == 'observe'