@@ -311,7 +311,13 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
311311
312312 @property
313313 def native_value (self ) -> float | None :
314- return self .coordinator .data .system .get ("poe_total_watts" ) if self .coordinator .data else None
314+ if not self .coordinator .data :
315+ return 0
316+ try :
317+ val = self .coordinator .data .system .get ("poe_total_watts" )
318+ return float (val ) if val is not None else None
319+ except (ValueError , TypeError ):
320+ return 0
315321
316322class BandwidthSensor (SwitchPortBaseEntity ):
317323 """Total bandwidth sensor."""
@@ -330,7 +336,13 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
330336 @property
331337 def native_value (self ) -> float | None :
332338 """Return the state of the sensor."""
333- return self .coordinator .data .bandwidth_mbps if self .coordinator .data else None
339+ if not self .coordinator .data :
340+ return 0
341+ try :
342+ val = self .coordinator .data .bandwidth_mbps
343+ return float (val ) if val is not None else None
344+ except (ValueError , TypeError ):
345+ return 0
334346
335347class FirmwareSensor (SwitchPortBaseEntity ):
336348 _attr_name = "Firmware"
@@ -342,7 +354,12 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
342354
343355 @property
344356 def native_value (self ) -> str | None :
345- return self .coordinator .data .system .get ("firmware" ) if self .coordinator .data else None
357+ if not self .coordinator .data :
358+ return ""
359+ try :
360+ return self .coordinator .data .system .get ("firmware" )
361+ except (ValueError , TypeError ):
362+ return ""
346363
347364class PortStatusSensor (SwitchPortBaseEntity ):
348365 """Port status (on/off) sensor, acting as the primary port entity."""
@@ -364,8 +381,11 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str, port: int)
364381 def native_value (self ) -> str | None :
365382 """Return the state (on/off)."""
366383 if not self .coordinator .data :
367- return None
368- return self .coordinator .data .ports .get (self .port , {}).get ("status" )
384+ return ""
385+ try :
386+ return coordinator .data .ports .get (self .port , {}).get ("status" )
387+ except (ValueError , TypeError ):
388+ return ""
369389
370390 @property
371391 def icon (self ) -> str | None :
@@ -376,93 +396,97 @@ def icon(self) -> str | None:
376396 def extra_state_attributes (self ) -> dict [str , Any ]:
377397 if not self .coordinator .data :
378398 return {}
379- p = self .coordinator .data .ports .get (self .port , {})
380-
381- # === LIFETIME VALUES (always available) ===
382- raw_rx_bytes = p .get ("rx" , 0 )
383- raw_tx_bytes = p .get ("tx" , 0 )
384-
385- # === LIVE RATE CALCULATION (only if we have previous data) ===
386- now = datetime .now ().timestamp ()
387- rx_bps_live = 0
388- tx_bps_live = 0
389-
390- if (self ._last_rx_bytes is not None
391- and self ._last_tx_bytes is not None
392- and self ._last_update is not None
393- and now > self ._last_update ):
394-
395- actual_delta = now - self ._last_update
396- delta_time = actual_delta if actual_delta < (self .coordinator .update_seconds * 1.5 ) else self .coordinator .update_seconds
397-
398- if delta_time > 0 :
399- # --- RAW DELTAS ---
400- delta_rx = raw_rx_bytes - self ._last_rx_bytes
401- delta_tx = raw_tx_bytes - self ._last_tx_bytes
402-
403- # --- HANDLE 32-bit WRAPAROUND ---
404- # Most switches use 32-bit counters for ifHC* until > 4GB
405- MAX32 = 4294967296 # 2^32
406-
407- if delta_rx < 0 :
408- # If previous value was "close" to wrap limit → wrap happened
409- if self ._last_rx_bytes > 3_000_000_000 :
410- delta_rx = (MAX32 - self ._last_rx_bytes ) + raw_rx_bytes
411-
412- if delta_tx < 0 :
413- if self ._last_tx_bytes > 3_000_000_000 :
414- delta_tx = (MAX32 - self ._last_tx_bytes ) + raw_tx_bytes
415-
416-
417- # --- COMPUTE LIVE BPS ---
418- rx_bps_live = int (delta_rx * 8 / delta_time )
419- tx_bps_live = int (delta_tx * 8 / delta_time )
420-
421- # --- FINAL SAFETY CLAMP ---
422- MAX_SAFE_BPS = 20_000_000_000
423- if rx_bps_live < 0 or rx_bps_live > MAX_SAFE_BPS :
424- _LOGGER .warning ("RX counter reset or spurious data detected. Dropping rate data." )
425- rx_bps_live = 0
426-
427- if tx_bps_live < 0 or tx_bps_live > MAX_SAFE_BPS :
428- _LOGGER .warning ("TX counter reset or spurious data detected. Dropping rate data." )
429- tx_bps_live = 0
430-
431- # Store for next poll
432- self ._last_rx_bytes = raw_rx_bytes
433- self ._last_tx_bytes = raw_tx_bytes
434- self ._last_update = now
435- port_info = self .coordinator .port_mapping .get (int (self .port ), {})
436- has_poe = (
437- p .get ("poe_power" , 0 ) > 0 or
438- p .get ("poe_status" , 0 ) > 0 or
439- self .coordinator .base_oids .get ("poe_power" ) or
440- self .coordinator .base_oids .get ("poe_status" )
441- )
442- attrs = {
443- "port_name" : p .get ("name" ),
444- "speed_bps" : p .get ("speed" ),
445- # Legacy — kept for old cards / backward compatibility
446- "rx_bps" : raw_rx_bytes * 8 ,
447- "tx_bps" : raw_tx_bytes * 8 ,
448- # NEW — real live rates (used when card has show_live_traffic: true)
449- "rx_bps_live" : rx_bps_live ,
450- "tx_bps_live" : tx_bps_live ,
451- # SFP / Copper detection (universal — works on Zyxel, TP-Link, QNAP, ASUS, etc.)
452- "is_sfp" : bool (port_info .get ("is_sfp" , False )),
453- "is_copper" : bool (port_info .get ("is_copper" , True )),
454- "interface" : port_info .get ("if_descr" ), # e.g. "eth5"
455- "custom" : p .get ("port_custom" ),
456- }
457- if self .coordinator .include_vlans and p .get ("vlan" ) is not None :
458- attrs ["vlan_id" ] = p ["vlan" ]
459- if has_poe :
460- attrs .update ({
461- "poe_power_watts" : round (p .get ("poe_power" , 0 ) / 1000.0 , 2 ),
462- "poe_enabled" : p .get ("poe_status" ) in (1 , 2 , 4 ),
463- "poe_class" : p .get ("poe_status" ),
464- })
465- return attrs
399+ try :
400+ p = self .coordinator .data .ports .get (self .port , {})
401+
402+ # === LIFETIME VALUES (always available) ===
403+ raw_rx_bytes = p .get ("rx" , 0 )
404+ raw_tx_bytes = p .get ("tx" , 0 )
405+
406+ # === LIVE RATE CALCULATION (only if we have previous data) ===
407+ now = datetime .now ().timestamp ()
408+ rx_bps_live = 0
409+ tx_bps_live = 0
410+
411+ if (self ._last_rx_bytes is not None
412+ and self ._last_tx_bytes is not None
413+ and self ._last_update is not None
414+ and now > self ._last_update ):
415+
416+ actual_delta = now - self ._last_update
417+ delta_time = actual_delta if actual_delta < (self .coordinator .update_seconds * 1.5 ) else self .coordinator .update_seconds
418+
419+ if delta_time > 0 :
420+ # --- RAW DELTAS ---
421+ delta_rx = raw_rx_bytes - self ._last_rx_bytes
422+ delta_tx = raw_tx_bytes - self ._last_tx_bytes
423+
424+ # --- HANDLE 32-bit WRAPAROUND ---
425+ # Most switches use 32-bit counters for ifHC* until > 4GB
426+ MAX32 = 4294967296 # 2^32
427+
428+ if delta_rx < 0 :
429+ # If previous value was "close" to wrap limit → wrap happened
430+ if self ._last_rx_bytes > 3_000_000_000 :
431+ delta_rx = (MAX32 - self ._last_rx_bytes ) + raw_rx_bytes
432+
433+ if delta_tx < 0 :
434+ if self ._last_tx_bytes > 3_000_000_000 :
435+ delta_tx = (MAX32 - self ._last_tx_bytes ) + raw_tx_bytes
436+
437+
438+ # --- COMPUTE LIVE BPS ---
439+ rx_bps_live = int (delta_rx * 8 / delta_time )
440+ tx_bps_live = int (delta_tx * 8 / delta_time )
441+
442+ # --- FINAL SAFETY CLAMP ---
443+ MAX_SAFE_BPS = 20_000_000_000
444+ if rx_bps_live < 0 or rx_bps_live > MAX_SAFE_BPS :
445+ _LOGGER .warning ("RX counter reset or spurious data detected. Dropping rate data." )
446+ rx_bps_live = 0
447+
448+ if tx_bps_live < 0 or tx_bps_live > MAX_SAFE_BPS :
449+ _LOGGER .warning ("TX counter reset or spurious data detected. Dropping rate data." )
450+ tx_bps_live = 0
451+
452+ # Store for next poll
453+ self ._last_rx_bytes = raw_rx_bytes
454+ self ._last_tx_bytes = raw_tx_bytes
455+ self ._last_update = now
456+ port_info = self .coordinator .port_mapping .get (int (self .port ), {})
457+ has_poe = (
458+ p .get ("poe_power" , 0 ) > 0 or
459+ p .get ("poe_status" , 0 ) > 0 or
460+ self .coordinator .base_oids .get ("poe_power" ) or
461+ self .coordinator .base_oids .get ("poe_status" )
462+ )
463+ attrs = {
464+ "port_name" : p .get ("name" ),
465+ "speed_bps" : p .get ("speed" ),
466+ # Legacy — kept for old cards / backward compatibility
467+ "rx_bps" : raw_rx_bytes * 8 ,
468+ "tx_bps" : raw_tx_bytes * 8 ,
469+ # NEW — real live rates (used when card has show_live_traffic: true)
470+ "rx_bps_live" : rx_bps_live ,
471+ "tx_bps_live" : tx_bps_live ,
472+ # SFP / Copper detection (universal — works on Zyxel, TP-Link, QNAP, ASUS, etc.)
473+ "is_sfp" : bool (port_info .get ("is_sfp" , False )),
474+ "is_copper" : bool (port_info .get ("is_copper" , True )),
475+ "interface" : port_info .get ("if_descr" ), # e.g. "eth5"
476+ "custom" : p .get ("port_custom" ),
477+ }
478+ if self .coordinator .include_vlans and p .get ("vlan" ) is not None :
479+ attrs ["vlan_id" ] = p ["vlan" ]
480+ if has_poe :
481+ attrs .update ({
482+ "poe_power_watts" : round (p .get ("poe_power" , 0 ) / 1000.0 , 2 ),
483+ "poe_enabled" : p .get ("poe_status" ) in (1 , 2 , 4 ),
484+ "poe_class" : p .get ("poe_status" ),
485+ })
486+ return attrs
487+ except Exception as e :
488+ _LOGGER .debug ("Error calculating live traffic for port %s: %s" , self .port , e )
489+ return {}
466490
467491# --- System Sensors ---
468492
@@ -483,11 +507,11 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
483507 def native_value (self ) -> float | None :
484508 """Return the state of the sensor."""
485509 if not self .coordinator .data :
486- return None
510+ return 0
487511 try :
488512 return float (self .coordinator .data .system .get ("cpu" ) or 0 )
489513 except (ValueError , TypeError ):
490- return None
514+ return 0
491515
492516class CustomValueSensor (SwitchPortBaseEntity ):
493517 _attr_name = "Custom Value"
@@ -501,8 +525,11 @@ def __init__(self, coordinator, entry_id):
501525 def native_value (self ):
502526 """Return the custom OID value safely."""
503527 if not self .coordinator .data :
504- return None
505- return self .coordinator .data .system .get ("custom" )
528+ return ""
529+ try :
530+ return self .coordinator .data .system .get ("custom" )
531+ except (ValueError , TypeError ):
532+ return ""
506533
507534class SystemMemorySensor (SwitchPortBaseEntity ):
508535 """Memory usage sensor."""
@@ -521,11 +548,11 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
521548 def native_value (self ) -> float | None :
522549 """Return the state of the sensor."""
523550 if not self .coordinator .data :
524- return None
551+ return 0
525552 try :
526553 return float (self .coordinator .data .system .get ("memory" ) or 0 )
527554 except (ValueError , TypeError ):
528- return None
555+ return 0
529556
530557
531558class SystemUptimeSensor (SwitchPortBaseEntity ):
@@ -544,13 +571,13 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
544571 def native_value (self ) -> int | None :
545572 """Return the state of the sensor (in seconds)."""
546573 if not self .coordinator .data :
547- return None
574+ return 0
548575 try :
549576 # Uptime OID typically returns hundredths of a second. Convert to seconds.
550577 uptime_hsec = int (self .coordinator .data .system .get ("uptime" ) or 0 )
551578 return int (uptime_hsec / 100 )
552579 except (ValueError , TypeError ):
553- return None
580+ return 0
554581
555582
556583class SystemHostnameSensor (SwitchPortBaseEntity ):
@@ -567,8 +594,11 @@ def __init__(self, coordinator: SwitchPortCoordinator, entry_id: str) -> None:
567594 def native_value (self ) -> str | None :
568595 """Return the state of the sensor."""
569596 if not self .coordinator .data :
570- return None
571- return self .coordinator .data .system .get ("hostname" )
597+ return ""
598+ try :
599+ return self .coordinator .data .system .get ("hostname" )
600+ except (ValueError , TypeError ):
601+ return ""
572602
573603
574604# =============================================================================
0 commit comments