2626)
2727from homeassistant .core import HomeAssistant , callback
2828from homeassistant .helpers .device_registry import DeviceInfo
29+ from homeassistant .helpers .entity import EntityCategory
2930from homeassistant .helpers .entity_platform import AddEntitiesCallback
3031from homeassistant .helpers .update_coordinator import CoordinatorEntity
3132
@@ -122,25 +123,51 @@ async def async_setup_entry(
122123 """Create sensors for all currently-known channels and watch for new ones."""
123124 coordinator : EacCoordinator = hass .data [DOMAIN ][entry .entry_id ]
124125
125- known : set [tuple [str , str ]] = set () # (service_point_id, channel_id)
126+ known_channels : set [tuple [str , str ]] = set () # (service_point_id, channel_id)
127+ known_diagnostic : set [str ] = set () # service_point_id
126128
127129 @callback
128130 def _add_new_entities () -> None :
129- new_entities : list [EacChannelSensor ] = []
131+ new_entities : list [CoordinatorEntity [ EacCoordinator ] ] = []
130132 for sp_state in coordinator .data .points .values ():
131133 for ch_state in sp_state .channels .values ():
132134 key = (sp_state .service_point .id , ch_state .channel .id )
133- if key in known :
135+ if key in known_channels :
134136 continue
135- known .add (key )
136- new_entities .append (EacChannelSensor (coordinator , sp_state , ch_state .channel .id ))
137+ known_channels .add (key )
138+ new_entities .append (
139+ EacChannelSensor (coordinator , sp_state , ch_state .channel .id )
140+ )
141+ # One diagnostic sensor per device (= per active service point that
142+ # produced a meter config). Inactive points don't get a device, so
143+ # they don't get one either.
144+ if (
145+ sp_state .active_meter is not None
146+ and sp_state .service_point .id not in known_diagnostic
147+ ):
148+ known_diagnostic .add (sp_state .service_point .id )
149+ new_entities .append (EacLastUpdateSensor (coordinator , sp_state ))
137150 if new_entities :
138151 async_add_entities (new_entities )
139152
140153 _add_new_entities ()
141154 entry .async_on_unload (coordinator .async_add_listener (_add_new_entities ))
142155
143156
157+ def _device_info_for (sp_state : ServicePointState ) -> DeviceInfo :
158+ meter = sp_state .active_meter
159+ return DeviceInfo (
160+ identifiers = {(DOMAIN , sp_state .service_point .id )},
161+ name = sp_state .service_point .address or f"EAC { sp_state .service_point .id } " ,
162+ manufacturer = meter .manufacturer if meter else MANUFACTURER ,
163+ model = meter .model if meter else None ,
164+ serial_number = meter .serial_number if meter else sp_state .service_point .serial_number ,
165+ configuration_url = (
166+ f"https://meterreading-dso.eac.com.cy/sp/{ sp_state .service_point .id } "
167+ ),
168+ )
169+
170+
144171class EacChannelSensor (CoordinatorEntity [EacCoordinator ], SensorEntity ):
145172 """One reading channel of one service point."""
146173
@@ -160,15 +187,7 @@ def __init__(
160187 self .entity_description = _description_for (ch .uom , ch .type , ch .interval )
161188 self ._attr_unique_id = f"{ self ._sp_id } _{ self ._channel_id } "
162189
163- meter = sp_state .active_meter
164- self ._attr_device_info = DeviceInfo (
165- identifiers = {(DOMAIN , self ._sp_id )},
166- name = sp_state .service_point .address or f"EAC { self ._sp_id } " ,
167- manufacturer = meter .manufacturer if meter else MANUFACTURER ,
168- model = meter .model if meter else None ,
169- serial_number = meter .serial_number if meter else sp_state .service_point .serial_number ,
170- configuration_url = f"https://meterreading-dso.eac.com.cy/sp/{ self ._sp_id } " ,
171- )
190+ self ._attr_device_info = _device_info_for (sp_state )
172191
173192 @property
174193 def _ch_state (self ):
@@ -219,3 +238,32 @@ def extra_state_attributes(self) -> dict[str, Any]:
219238
220239def _iso (dt : datetime | None ) -> str | None :
221240 return dt .isoformat () if dt is not None else None
241+
242+
243+ class EacLastUpdateSensor (CoordinatorEntity [EacCoordinator ], SensorEntity ):
244+ """Diagnostic timestamp showing when the coordinator last refreshed.
245+
246+ Lets users distinguish "polling is alive but the DSO has no fresh data"
247+ from "polling is broken" without digging into logs. Reports the same
248+ value across every device the integration owns — there is one polling
249+ cycle for the whole config entry.
250+ """
251+
252+ _attr_has_entity_name = True
253+ _attr_entity_category = EntityCategory .DIAGNOSTIC
254+ _attr_device_class = SensorDeviceClass .TIMESTAMP
255+ _attr_name = "Last successful update"
256+
257+ def __init__ (
258+ self , coordinator : EacCoordinator , sp_state : ServicePointState
259+ ) -> None :
260+ super ().__init__ (coordinator )
261+ self ._sp_id = sp_state .service_point .id
262+ self ._attr_unique_id = f"{ self ._sp_id } _last_update"
263+ self ._attr_device_info = _device_info_for (sp_state )
264+
265+ @property
266+ def native_value (self ) -> datetime | None :
267+ if self .coordinator .data is None :
268+ return None
269+ return self .coordinator .data .last_success_time
0 commit comments