From e37ef478d14a59409486ab23de6b2e747fd32d72 Mon Sep 17 00:00:00 2001 From: Frank Mainz Date: Wed, 21 Oct 2026 21:53:17 +0200 Subject: [PATCH] Add DHCP, traffic, and update sensors --- README-de.md | 54 ++++++-- README.md | 52 ++++++-- custom_components/ipfire/binary_sensor.py | 9 +- custom_components/ipfire/coordinator.py | 86 ++++++++++++- custom_components/ipfire/icons.json | 26 +++- custom_components/ipfire/manifest.json | 2 +- custom_components/ipfire/sensor.py | 120 +++++++++++++++++- custom_components/ipfire/translations/de.json | 37 +++++- custom_components/ipfire/translations/en.json | 37 +++++- 9 files changed, 371 insertions(+), 52 deletions(-) diff --git a/README-de.md b/README-de.md index 9879bc2..a9de650 100644 --- a/README-de.md +++ b/README-de.md @@ -41,6 +41,12 @@ Diese Dokumentation ist auch in 🇬🇧 [englischer Sprache](README.md) verfüg ## Neues in dieser Version +### v0.3.6 + +* DHCP-Lease-Anzahl und Clientdaten als Sensorattribute hinzugefügt (benötigt api.cgi). +* Kumulative Trafficzähler für die GREEN-, ORANGE- und BLUE-Schnittstellen hinzugefügt (benötigt api.cgi). +* Sensor für erforderlichen Neustart sowie Gesamtzahl verfügbarer Core- und Paket-Updates hinzugefügt (benötigt api.cgi). + ### v0.3.5 * Die Entwicklung des IPFire-CGI `api.cgi` wird im eigenständigen Repository [IPFire-API](https://github.com/FMainz/IPFire-API) fortgeführt. @@ -65,19 +71,24 @@ Diese Dokumentation ist auch in 🇬🇧 [englischer Sprache](README.md) verfüg HA-IPFire bietet folgende Funktionen: -* Download-Trafficzähler -* Upload-Trafficzähler +* RED-Download-Trafficzähler +* RED-Upload-Trafficzähler * Aktuelle Download-Geschwindigkeit * Aktuelle Upload-Geschwindigkeit -* Status der Internetverbindung -* Dauer der Internetverbindung -* Connect- und Disconnect-Steuerung -* Konfigurierbares Abfrageintervall +* Status der Internetverbindung¹ +* Dauer der Internetverbindung¹ +* Connect- und Disconnect-Steuerung¹ +* Konfigurierbares Abfrageintervall¹ * Abfrageintervall von 5 bis 60 Sekunden * Optionale SSL-Zertifikatsprüfung * Authentifizierung mit Benutzername und Passwort * HACS-kompatibel * Deutsche und englische Übersetzungen +* DHCP-Lease-Sensor mit Clientdaten¹ +* Trafficzähler für einzelne Netzwerkzonen¹ +* Neustartstatus und Gesamtzahl verfügbarer Updates¹ + +¹Benötigt api.cgi aus [IPFire-API](https://github.com/FMainz/IPFire-API) ## IPFire @@ -106,11 +117,12 @@ Zusätzlich zum Verbindungsstatus und den Steuerungsaktionen liefert die API unt * IPFire-, Pakfire- und Kernel-Version sowie Systemhersteller, -modell und Architektur * CPU-Modell und Anzahl der Kerne * Arbeitsspeicher und Festplattenbelegung einschließlich I/O-Statistiken und SMART-Status -* Status der Netzwerkschnittstellen und IPFire-Dienste +* Status der Netzwerkschnittstellen, Trafficzähler pro Zone und IPFire-Dienste +* Aktive DHCP-Leases mit Clientdaten * installierten Add-ons und deren Status -* verfügbaren Core- und Paket-Updates +* Core- und Paket-Updates sowie erforderlicher Neustart -Eine vollständige Beschreibung der JSON-Felder findest du in der [IPFire-API-Dokumentation](https://github.com/FMainz/IPFire-API/blob/main/README-de.md#endpunkt). +Eine vollständige Beschreibung der JSON-Felder findest du in der [IPFire-API-Dokumentation](https://github.com/FMainz/IPFire-API/blob/main/README-de.md#api-zugriff). ### Verbindungssteuerung @@ -152,12 +164,29 @@ Die Integration stellt folgende Sensoren bereit: ### Traffic -* **Download** – kumulativer Download-Traffic in Byte. -* **Upload** – kumulativer Upload-Traffic in Byte. +* **Rot empfangen** – kumulativ empfangene Bytes auf der RED-Schnittstelle. +* **Rot gesendet** – kumulativ gesendete Bytes auf der RED-Schnittstelle. * **Download Speed** – aktuelle Download-Geschwindigkeit. * **Upload Speed** – aktuelle Upload-Geschwindigkeit. -Die folgenden Sensoren und Funktionen erfordern, dass `api.cgi` auf der IPFire-Firewall [installiert](#installation-des-ipfire-cgi) ist. +Die folgenden zusätzlichen Sensoren und Funktionen erfordern, dass `api.cgi` auf der IPFire-Firewall [installiert](#installation-des-ipfire-cgi) ist. + +### Traffic interner Netzwerkschnittstellen + +* **Grün empfangen/gesendet** – kumulativ empfangene und gesendete Bytes auf der GREEN-Schnittstelle. +* **Orange empfangen/gesendet** – kumulativ empfangene und gesendete Bytes auf der ORANGE-Schnittstelle. +* **Blau empfangen/gesendet** – kumulativ empfangene und gesendete Bytes auf der BLUE-Schnittstelle. + +Sensoren werden nur für interne Schnittstellen angelegt, die auf der IPFire-Firewall konfiguriert sind. + +### DHCP-Leases + +* **Aktive DHCP-Leases** – Anzahl aktiver DHCP-Leases. Das Attribut `leases` enthält die von IPFire gemeldeten Clientdaten mit IP-Adresse, MAC-Adresse, Hostname, Ablaufzeit und Status. Du findest sie in Home Assistant in der Detailansicht des Sensors unter den Attributen. + +### Systemstatus + +* **Neustart erforderlich** – zeigt an, wenn IPFire nach einem Update einen Neustart verlangt. Benötigt eine aktuelle Version von `api.cgi`; bei älteren Versionen bleibt der Sensor unbekannt. +* **Updates verfügbar** – Gesamtzahl der verfügbaren Updates, einschließlich Core- und Paket-Updates. ### Internetverbindung @@ -178,7 +207,6 @@ Die folgenden Sensoren und Funktionen erfordern, dass `api.cgi` auf der IPFire-F * **Model** * **Vendor** * **Uptime** -* **Package Updates** ### CPU diff --git a/README.md b/README.md index 93ad432..e95b4c0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ This documentation is also available in 🇩🇪 [German](README-de.md). ## What’s New in This Version +### v0.3.6 + +* Added DHCP lease count and client details as sensor attributes (requires `api.cgi`). +* Added cumulative traffic counters for the GREEN, ORANGE, and BLUE interfaces (requires `api.cgi`). +* Added a reboot-required sensor and a combined count of available core and package updates (requires `api.cgi`). + ### v0.3.5 * Development of the IPFire CGI `api.cgi` continues in the standalone [IPFire-API](https://github.com/FMainz/IPFire-API) repository. @@ -65,19 +71,24 @@ This documentation is also available in 🇩🇪 [German](README-de.md). HA-IPFire provides the following features: -* Download traffic counter -* Upload traffic counter +* RED download traffic counter +* RED upload traffic counter * Current download speed * Current upload speed -* Internet connection state -* Internet connection duration -* Connect and Disconnect controls -* Configurable polling interval +* Internet connection state¹ +* Internet connection duration¹ +* Connect and Disconnect controls¹ +* Configurable polling interval¹ * Polling interval from 5 to 60 seconds * Optional SSL certificate verification * Username/password authentication * HACS compatible * German and English translations +* DHCP lease sensor with client details¹ +* Traffic counters for individual network zones¹ +* Reboot-required status and total available update count¹ + +¹Requires `api.cgi` from [IPFire-API](https://github.com/FMainz/IPFire-API). ## IPFire @@ -108,9 +119,10 @@ In addition to connection status and controls, the API provides information such * IPFire, Pakfire, and kernel versions, plus system vendor, model, and architecture * CPU model and number of cores * Memory and disk usage, including I/O statistics and SMART status -* Network interface and IPFire service states +* Network interface states and per-zone traffic counters, plus IPFire service states +* Active DHCP leases and their client details * Installed add-ons and their status -* Available core and package updates +* Core and package update status, plus whether a reboot is required For a complete description of the JSON fields, see the [IPFire-API documentation](https://github.com/FMainz/IPFire-API/blob/main/README.md#endpoint). @@ -154,12 +166,29 @@ The integration provides the following sensors: ### Traffic -* **Download** – cumulative download traffic in bytes. -* **Upload** – cumulative upload traffic in bytes. +* **Red Traffic Received** – cumulative received bytes on the RED interface. +* **Red Traffic Sent** – cumulative sent bytes on the RED interface. * **Download Speed** – current download speed. * **Upload Speed** – current upload speed. -The following sensors and functions require `api.cgi` to be [installed](#ipfire-cgi-installation) on the IPFire firewall. +The following additional sensors and functions require `api.cgi` to be [installed](#ipfire-cgi-installation) on the IPFire firewall. + +### Internal network traffic + +* **Green Traffic Received/Sent** – cumulative received and sent bytes on the GREEN interface. +* **Orange Traffic Received/Sent** – cumulative received and sent bytes on the ORANGE interface. +* **Blue Traffic Received/Sent** – cumulative received and sent bytes on the BLUE interface. + +Sensors are created only for internal interfaces configured on the IPFire firewall. + +### DHCP leases + +* **Active DHCP Leases** – number of active DHCP leases. The `leases` attribute contains the client records reported by IPFire, including IP address, MAC address, hostname, expiry, and status. To view them, open the sensor's more-info panel in Home Assistant and inspect its attributes. + +### System status + +* **Reboot Required** – indicates when IPFire requires a reboot after an update. Requires a current version of `api.cgi`; older versions leave the sensor unknown. +* **Updates Available** – total number of available updates, including core and package updates. ### Internet connection @@ -180,7 +209,6 @@ The following sensors and functions require `api.cgi` to be [installed](#ipfire- * **Model** * **Vendor** * **Uptime** -* **Package Updates** ### CPU diff --git a/custom_components/ipfire/binary_sensor.py b/custom_components/ipfire/binary_sensor.py index a60bb63..12a7ea7 100644 --- a/custom_components/ipfire/binary_sensor.py +++ b/custom_components/ipfire/binary_sensor.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -36,11 +37,11 @@ class IPFireBinarySensorDescription(BinarySensorEntityDescription): entity_registry_enabled_default=False, ), IPFireBinarySensorDescription( - key="core_update", - translation_key="core_update", - value_key="core_update", + key="reboot_required", + translation_key="reboot_required", + value_key="reboot_required", data_group="system", - entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, ), IPFireBinarySensorDescription( key="network_blue", diff --git a/custom_components/ipfire/coordinator.py b/custom_components/ipfire/coordinator.py index 49b5de9..aefa4ef 100644 --- a/custom_components/ipfire/coordinator.py +++ b/custom_components/ipfire/coordinator.py @@ -32,8 +32,9 @@ class IPFireSystemData: model: str vendor: str virtual: bool - core_update: bool - package_updates: int + core_update: bool | None + reboot_required: bool | None + package_updates: int | None uptime: int | None cpu_total: int | None cpu_idle: int | None @@ -50,6 +51,18 @@ class IPFireSystemData: smart_temperature: int | None smart_errors: int | None + @property + def updates_available(self) -> int | None: + """Return the total number of available core and package updates.""" + if ( + not isinstance(self.core_update, bool) + or not isinstance(self.package_updates, int) + or isinstance(self.package_updates, bool) + ): + return None + + return self.package_updates + int(self.core_update) + @dataclass(frozen=True, slots=True) class IPFireNetworkData: @@ -59,6 +72,16 @@ class IPFireNetworkData: green: bool | None orange: bool | None red: bool | None + interfaces: dict[str, IPFireInterfaceData | None] + + +@dataclass(frozen=True, slots=True) +class IPFireInterfaceData: + """Represent byte counters for one IPFire network interface.""" + + device: str | None + rx_bytes: int | None + tx_bytes: int | None @dataclass(frozen=True, slots=True) @@ -78,6 +101,7 @@ class IPFireData: external_hostname: str system: IPFireSystemData network: IPFireNetworkData + dhcp_leases: tuple[dict[str, object], ...] | None services: dict[str, bool] addons: dict[str, dict[str, bool]] @@ -181,6 +205,7 @@ async def _async_update_data(self) -> IPFireData: traffic = payload["traffic"] system = payload.get("system", {}) network = payload.get("network", {}) + dhcp = payload.get("dhcp", {}) services = payload.get("services", {}) addons = payload.get("addons", {}) if not isinstance(connection, dict): @@ -195,11 +220,54 @@ async def _async_update_data(self) -> IPFireData: if not isinstance(network, dict): raise ValueError("Invalid network data from IPFire") + if not isinstance(dhcp, dict): + raise ValueError("Invalid DHCP data from IPFire") + if not isinstance(services, dict): raise ValueError("Invalid services data from IPFire") if not isinstance(addons, dict): raise ValueError("Invalid addons data from IPFire") + + raw_interfaces = network.get("interfaces", {}) + if not isinstance(raw_interfaces, dict): + raise ValueError("Invalid network interface data from IPFire") + + interfaces: dict[str, IPFireInterfaceData | None] = {} + for zone in ("red", "green", "orange", "blue"): + raw_interface = raw_interfaces.get(zone) + if raw_interface is None: + interfaces[zone] = None + continue + if not isinstance(raw_interface, dict): + raise ValueError( + f"Invalid {zone.upper()} interface data from IPFire" + ) + interfaces[zone] = IPFireInterfaceData( + device=( + str(raw_interface["device"]) + if raw_interface.get("device") is not None + else None + ), + rx_bytes=self._parse_optional_int( + raw_interface.get("rx_bytes") + ), + tx_bytes=self._parse_optional_int( + raw_interface.get("tx_bytes") + ), + ) + + raw_leases = dhcp.get("leases") + if raw_leases is not None and not isinstance(raw_leases, list): + raise ValueError("Invalid DHCP lease data from IPFire") + if isinstance(raw_leases, list) and not all( + isinstance(lease, dict) for lease in raw_leases + ): + raise ValueError("Invalid DHCP lease entry from IPFire") + dhcp_leases = ( + tuple(raw_leases) if isinstance(raw_leases, list) else None + ) + connection_state = str(connection.get("state", "unavailable")) if "rx_bytes" not in traffic: @@ -268,8 +336,15 @@ async def _async_update_data(self) -> IPFireData: model=system.get("model"), vendor=system.get("vendor"), virtual=system.get("virtual"), - core_update=system.get("core_update"), - package_updates=system.get("package_updates"), + core_update=( + system.get("core_update") + if isinstance(system.get("core_update"), bool) + else None + ), + reboot_required=system.get("reboot_required"), + package_updates=self._parse_optional_int( + system.get("package_updates") + ), uptime=system.get("uptime"), cpu_total=cpu_total, cpu_idle=cpu_idle, @@ -291,6 +366,7 @@ async def _async_update_data(self) -> IPFireData: green=network.get("green"), orange=network.get("orange"), red=network.get("red"), + interfaces=interfaces, ) except (KeyError, TypeError, ValueError) as err: @@ -338,6 +414,7 @@ async def _async_update_data(self) -> IPFireData: external_hostname=external_hostname, system=system_data, network=network_data, + dhcp_leases=dhcp_leases, services=services, addons=addons, ) @@ -419,6 +496,7 @@ def _parse_speed_cgi(payload: str) -> dict: }, "system": {}, "network": {}, + "dhcp": {}, "services": {}, "addons": {}, } diff --git a/custom_components/ipfire/icons.json b/custom_components/ipfire/icons.json index a7af86e..15cbe7c 100644 --- a/custom_components/ipfire/icons.json +++ b/custom_components/ipfire/icons.json @@ -13,6 +13,27 @@ "upload_rate": { "default": "mdi:upload-network" }, + "dhcp_active_leases": { + "default": "mdi:lan" + }, + "green_rx_bytes": { + "default": "mdi:download-network" + }, + "green_tx_bytes": { + "default": "mdi:upload-network" + }, + "orange_rx_bytes": { + "default": "mdi:download-network" + }, + "orange_tx_bytes": { + "default": "mdi:upload-network" + }, + "blue_rx_bytes": { + "default": "mdi:download-network" + }, + "blue_tx_bytes": { + "default": "mdi:upload-network" + }, "connection_duration": { "default": "mdi:timer-outline" }, @@ -100,11 +121,14 @@ "smart_errors": { "default": "mdi:alert-circle" }, - "package_updates": { + "updates_available": { "default": "mdi:package-up" } }, "binary_sensor": { + "reboot_required": { + "default": "mdi:restart-alert" + }, "service_dhcp": { "default": "mdi:ip-network" }, diff --git a/custom_components/ipfire/manifest.json b/custom_components/ipfire/manifest.json index dcf2495..589fe6d 100644 --- a/custom_components/ipfire/manifest.json +++ b/custom_components/ipfire/manifest.json @@ -10,5 +10,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/FMainz/HA-IPFire/issues", "requirements": [], - "version": "0.3.5" + "version": "0.3.6" } diff --git a/custom_components/ipfire/sensor.py b/custom_components/ipfire/sensor.py index 079081d..13644f2 100644 --- a/custom_components/ipfire/sensor.py +++ b/custom_components/ipfire/sensor.py @@ -322,10 +322,9 @@ class IPFireSensorDescription(SensorEntityDescription): ), IPFireSensorDescription( key="package_updates", - translation_key="package_updates", - value_key="package_updates", + translation_key="updates_available", + value_key="updates_available", data_group="system", - entity_registry_enabled_default=False, ), ) @@ -339,14 +338,30 @@ async def async_setup_entry( coordinator: IPFireCoordinator = entry.runtime_data.coordinator - async_add_entities( + entities: list[SensorEntity] = [ IPFireTrafficSensor( coordinator, entry, description, ) for description in SENSORS - ) + ] + + entities.append(IPFireDHCPLeasesSensor(coordinator, entry)) + + # The overall Download/Upload sensors represent the RED (WAN) interface. + # Add separate byte counters for the internal GREEN, ORANGE, and BLUE zones. + if coordinator.data is not None: + for zone in ("green", "orange", "blue"): + interface = coordinator.data.network.interfaces.get(zone) + if interface is None or interface.device is None: + continue + entities.extend( + IPFireInterfaceTrafficSensor(coordinator, entry, zone, direction) + for direction in ("rx", "tx") + ) + + async_add_entities(entities) class IPFireTrafficSensor( @@ -396,3 +411,98 @@ def native_value(self) -> int | float | str | None: return dt_util.utc_from_timestamp(value) return value + + +class IPFireDHCPLeasesSensor( + CoordinatorEntity[IPFireCoordinator], + SensorEntity, +): + """Report the number of active DHCP leases and expose the lease list.""" + + _attr_has_entity_name = True + _attr_translation_key = "dhcp_active_leases" + _attr_entity_registry_enabled_default = True + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__( + self, + coordinator: IPFireCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the DHCP lease sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{entry.entry_id}_dhcp_active_leases" + self._attr_device_info = DeviceInfo( + identifiers={("ipfire", entry.entry_id)}, + name="IPFire", + manufacturer="IPFire", + model="Firewall", + ) + + @property + def native_value(self) -> int | None: + """Return the number of active leases.""" + if self.coordinator.data is None: + return None + leases = self.coordinator.data.dhcp_leases + if leases is None: + return None + return sum(lease.get("status") == "active" for lease in leases) + + @property + def extra_state_attributes(self) -> dict[str, list[dict[str, object]]] | None: + """Expose the lease records as sensor attributes.""" + if self.coordinator.data is None: + return None + leases = self.coordinator.data.dhcp_leases + if leases is None: + return None + return {"leases": [dict(lease) for lease in leases]} + + +class IPFireInterfaceTrafficSensor( + CoordinatorEntity[IPFireCoordinator], + SensorEntity, +): + """Report received or sent bytes for an internal IPFire interface.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: IPFireCoordinator, + entry: ConfigEntry, + zone: str, + direction: str, + ) -> None: + """Initialize an internal interface traffic sensor.""" + super().__init__(coordinator) + self._zone = zone + self._direction = direction + key = f"{zone}_{direction}_bytes" + self.entity_description = SensorEntityDescription( + key=key, + translation_key=key, + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, + suggested_display_precision=1, + ) + self._attr_unique_id = f"{entry.entry_id}_{key}" + self._attr_device_info = DeviceInfo( + identifiers={("ipfire", entry.entry_id)}, + name="IPFire", + manufacturer="IPFire", + model="Firewall", + ) + + @property + def native_value(self) -> int | None: + """Return the current byte counter for the interface.""" + if self.coordinator.data is None: + return None + interface = self.coordinator.data.network.interfaces.get(self._zone) + if interface is None: + return None + return getattr(interface, f"{self._direction}_bytes") diff --git a/custom_components/ipfire/translations/de.json b/custom_components/ipfire/translations/de.json index b5d3488..4cc7775 100644 --- a/custom_components/ipfire/translations/de.json +++ b/custom_components/ipfire/translations/de.json @@ -66,10 +66,10 @@ "entity": { "sensor": { "download": { - "name": "Download" + "name": "Rot empfangen" }, "upload": { - "name": "Upload" + "name": "Rot gesendet" }, "download_rate": { "name": "Download-Geschwindigkeit" @@ -77,6 +77,27 @@ "upload_rate": { "name": "Upload-Geschwindigkeit" }, + "dhcp_active_leases": { + "name": "Aktive DHCP-Leases" + }, + "green_rx_bytes": { + "name": "Grün empfangen" + }, + "green_tx_bytes": { + "name": "Grün gesendet" + }, + "orange_rx_bytes": { + "name": "Orange empfangen" + }, + "orange_tx_bytes": { + "name": "Orange gesendet" + }, + "blue_rx_bytes": { + "name": "Blau empfangen" + }, + "blue_tx_bytes": { + "name": "Blau gesendet" + }, "connection_duration": { "name": "Verbindungsdauer" }, @@ -163,8 +184,8 @@ "smart_errors": { "name": "SMART-Fehler" }, - "package_updates": { - "name": "Paket-Updates" + "updates_available": { + "name": "Updates verfügbar" }, "external_ip": { "name": "Externe IP-Adresse" @@ -177,8 +198,12 @@ "virtual": { "name": "Virtuell" }, - "core_update": { - "name": "Core-Update verfügbar" + "reboot_required": { + "name": "Neustart erforderlich", + "state": { + "on": "Ja", + "off": "Nein" + } }, "network_blue": { "name": "Blau (WLAN)" diff --git a/custom_components/ipfire/translations/en.json b/custom_components/ipfire/translations/en.json index 73d802f..85a85bc 100644 --- a/custom_components/ipfire/translations/en.json +++ b/custom_components/ipfire/translations/en.json @@ -66,10 +66,10 @@ "entity": { "sensor": { "download": { - "name": "Download" + "name": "Red Traffic Received" }, "upload": { - "name": "Upload" + "name": "Red Traffic Sent" }, "download_rate": { "name": "Download Speed" @@ -77,6 +77,27 @@ "upload_rate": { "name": "Upload Speed" }, + "dhcp_active_leases": { + "name": "Active DHCP Leases" + }, + "green_rx_bytes": { + "name": "Green Traffic Received" + }, + "green_tx_bytes": { + "name": "Green Traffic Sent" + }, + "orange_rx_bytes": { + "name": "Orange Traffic Received" + }, + "orange_tx_bytes": { + "name": "Orange Traffic Sent" + }, + "blue_rx_bytes": { + "name": "Blue Traffic Received" + }, + "blue_tx_bytes": { + "name": "Blue Traffic Sent" + }, "connection_duration": { "name": "Connection Duration" }, @@ -163,8 +184,8 @@ "smart_errors": { "name": "SMART Errors" }, - "package_updates": { - "name": "Package Updates" + "updates_available": { + "name": "Updates Available" }, "external_ip": { "name": "External IP Address" @@ -177,8 +198,12 @@ "virtual": { "name": "Virtual" }, - "core_update": { - "name": "Core Update Available" + "reboot_required": { + "name": "Reboot Required", + "state": { + "on": "Yes", + "off": "No" + } }, "network_blue": { "name": "Blue (WiFi)"