Skip to content

Commit c65a858

Browse files
authored
Update sensor.py
1 parent 6300244 commit c65a858

1 file changed

Lines changed: 132 additions & 102 deletions

File tree

  • custom_components/switch_port_card_pro

custom_components/switch_port_card_pro/sensor.py

Lines changed: 132 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -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

316322
class 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

335347
class 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

347364
class 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

492516
class 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

507534
class 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

531558
class 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

556583
class 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

Comments
 (0)