Implement network throughput monitoring instead of cumulative bytes
- Add iftop as runtime dependency in package.nix and flake.nix - Modify SystemMetrics to calculate network throughput (bytes/second) - Track previous network stats in MQTT client state for throughput calculation - Update Home Assistant discovery to show RX/TX throughput sensors - Replace cumulative byte counters with real-time throughput metrics - Add proper throughput calculation with time-based differentials This provides much more useful real-time network monitoring compared to ever-increasing cumulative byte counts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f8a8421c2f
commit
b8e7c48ecf
@ -43,6 +43,9 @@
|
||||
|
||||
# Mosquito for MQTT support
|
||||
mosquitto
|
||||
|
||||
# Network monitoring
|
||||
iftop
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
lib,
|
||||
beamPackages,
|
||||
src,
|
||||
iftop,
|
||||
}:
|
||||
|
||||
beamPackages.mixRelease rec {
|
||||
@ -10,6 +11,9 @@ beamPackages.mixRelease rec {
|
||||
|
||||
inherit src;
|
||||
|
||||
# Runtime dependencies
|
||||
buildInputs = [ iftop ];
|
||||
|
||||
# Disable distributed Erlang to avoid COOKIE requirement
|
||||
postInstall = ''
|
||||
# Create wrapper script that sets proper environment including COOKIE
|
||||
|
||||
@ -134,9 +134,9 @@ defmodule Systant.HaDiscovery do
|
||||
{"sensor", "disk_root_usage", build_sensor_config("Root Disk Usage", "#{base_topic}", "disk.disks", "%", "mdi:harddisk", "{{ (value_json.disk.disks | selectattr('mounted_on', 'equalto', '/') | list | first).use_percent if value_json.disk.disks else 0 }}")},
|
||||
{"sensor", "disk_home_usage", build_sensor_config("Home Disk Usage", "#{base_topic}", "disk.disks", "%", "mdi:harddisk", "{{ (value_json.disk.disks | selectattr('mounted_on', 'equalto', '/home') | list | first).use_percent if (value_json.disk.disks | selectattr('mounted_on', 'equalto', '/home') | list) else 0 }}")},
|
||||
|
||||
# Network Sensors - Primary interface
|
||||
{"sensor", "network_rx_bytes", build_sensor_config("Network RX Bytes", "#{base_topic}", "network", "bytes", "mdi:download-network", "{{ value_json.network[0].rx_bytes if value_json.network and value_json.network|length > 0 else 0 }}")},
|
||||
{"sensor", "network_tx_bytes", build_sensor_config("Network TX Bytes", "#{base_topic}", "network", "bytes", "mdi:upload-network", "{{ value_json.network[0].tx_bytes if value_json.network and value_json.network|length > 0 else 0 }}")},
|
||||
# Network Sensors - Primary interface throughput
|
||||
{"sensor", "network_rx_throughput", build_sensor_config("Network RX Throughput", "#{base_topic}", "network", "B/s", "mdi:download-network", "{{ value_json.network[0].rx_throughput_bps if value_json.network and value_json.network|length > 0 else 0 }}")},
|
||||
{"sensor", "network_tx_throughput", build_sensor_config("Network TX Throughput", "#{base_topic}", "network", "B/s", "mdi:upload-network", "{{ value_json.network[0].tx_throughput_bps if value_json.network and value_json.network|length > 0 else 0 }}")},
|
||||
|
||||
# Binary Sensors for status
|
||||
{"binary_sensor", "system_online", build_binary_sensor_config("System Online", "#{base_topic}", "mdi:server", "connectivity")}
|
||||
|
||||
@ -20,7 +20,8 @@ defmodule Systant.MqttClient do
|
||||
# Store both configs for later use
|
||||
state_config = %{
|
||||
app_config: app_config,
|
||||
mqtt_config: mqtt_config
|
||||
mqtt_config: mqtt_config,
|
||||
previous_network_stats: nil
|
||||
}
|
||||
|
||||
connection_opts = [
|
||||
@ -64,14 +65,14 @@ defmodule Systant.MqttClient do
|
||||
|
||||
def handle_info(:publish_startup_stats, state) do
|
||||
Logger.info("Publishing initial system metrics")
|
||||
publish_stats(state.app_config, state.mqtt_config)
|
||||
{:noreply, state}
|
||||
{_stats, updated_state} = collect_and_publish_stats(state)
|
||||
{:noreply, updated_state}
|
||||
end
|
||||
|
||||
def handle_info(:publish_stats, state) do
|
||||
publish_stats(state.app_config, state.mqtt_config)
|
||||
{_stats, updated_state} = collect_and_publish_stats(state)
|
||||
schedule_stats_publish(state.mqtt_config.publish_interval)
|
||||
{:noreply, state}
|
||||
{:noreply, updated_state}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
@ -83,6 +84,36 @@ defmodule Systant.MqttClient do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp collect_and_publish_stats(state) do
|
||||
# Collect metrics with previous network stats for throughput calculation
|
||||
stats = Systant.SystemMetrics.collect_metrics(state.app_config, state.previous_network_stats)
|
||||
|
||||
# Store current network stats for next iteration
|
||||
current_network_stats = case Map.get(stats, :network) do
|
||||
network_data when is_list(network_data) ->
|
||||
%{
|
||||
interfaces: network_data,
|
||||
timestamp: System.monotonic_time(:second)
|
||||
}
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
updated_state = Map.put(state, :previous_network_stats, current_network_stats)
|
||||
|
||||
# Publish the stats
|
||||
payload = Jason.encode!(stats)
|
||||
|
||||
case Tortoise.publish(state.mqtt_config.client_id, state.mqtt_config.stats_topic, payload, qos: state.mqtt_config.qos) do
|
||||
:ok ->
|
||||
Logger.info("Published system metrics for #{stats.hostname}")
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to publish stats: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
{stats, updated_state}
|
||||
end
|
||||
|
||||
# Legacy function for compatibility if needed
|
||||
defp publish_stats(app_config, mqtt_config) do
|
||||
stats = Systant.SystemMetrics.collect_metrics(app_config)
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ defmodule Systant.SystemMetrics do
|
||||
@doc """
|
||||
Collect system metrics based on configuration
|
||||
"""
|
||||
def collect_metrics(config \\ nil) do
|
||||
def collect_metrics(config \\ nil, previous_network_stats \\ nil) do
|
||||
config = config || Systant.Config.load_config()
|
||||
|
||||
base_metrics = %{
|
||||
@ -27,7 +27,7 @@ defmodule Systant.SystemMetrics do
|
||||
"memory" -> Map.put(acc, :memory, collect_memory_metrics(config))
|
||||
"disk" -> Map.put(acc, :disk, collect_disk_metrics(config))
|
||||
"gpu" -> Map.put(acc, :gpu, collect_gpu_metrics(config))
|
||||
"network" -> Map.put(acc, :network, collect_network_metrics(config))
|
||||
"network" -> Map.put(acc, :network, collect_network_metrics(config, previous_network_stats))
|
||||
"temperature" -> Map.put(acc, :temperature, collect_temperature_metrics(config))
|
||||
"processes" -> Map.put(acc, :processes, collect_process_metrics(config))
|
||||
"system" -> Map.put(acc, :system, collect_system_info(config))
|
||||
@ -73,10 +73,10 @@ defmodule Systant.SystemMetrics do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect network interface statistics
|
||||
Collect network interface statistics with throughput calculation
|
||||
"""
|
||||
def collect_network_metrics(config) do
|
||||
get_network_stats(config)
|
||||
def collect_network_metrics(config, previous_stats \\ nil) do
|
||||
get_network_stats(config, previous_stats)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -681,19 +681,23 @@ defmodule Systant.SystemMetrics do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_network_stats(config) do
|
||||
defp get_network_stats(config, previous_stats \\ nil) do
|
||||
network_config = Systant.Config.get(config, ["network"]) || %{}
|
||||
current_time = System.monotonic_time(:second)
|
||||
|
||||
try do
|
||||
case File.read("/proc/net/dev") do
|
||||
{:ok, content} ->
|
||||
content
|
||||
current_interfaces = content
|
||||
|> String.split("\n")
|
||||
|> Enum.drop(2)
|
||||
|> Enum.filter(&(String.trim(&1) != ""))
|
||||
|> Enum.map(&parse_network_interface/1)
|
||||
|> Enum.filter(&(&1 != nil))
|
||||
|> filter_network_interfaces(network_config)
|
||||
|> Enum.map(&calculate_throughput(&1, previous_stats, current_time))
|
||||
|
||||
current_interfaces
|
||||
_ -> []
|
||||
end
|
||||
rescue
|
||||
@ -794,4 +798,44 @@ defmodule Systant.SystemMetrics do
|
||||
end
|
||||
end
|
||||
|
||||
defp calculate_throughput(current_interface, previous_stats, current_time) do
|
||||
case previous_stats do
|
||||
%{interfaces: prev_interfaces, timestamp: prev_time} ->
|
||||
# Find matching interface in previous data
|
||||
prev_interface = Enum.find(prev_interfaces, &(&1.interface == current_interface.interface))
|
||||
|
||||
if prev_interface && prev_time do
|
||||
time_diff = current_time - prev_time
|
||||
|
||||
if time_diff > 0 do
|
||||
rx_bytes_diff = current_interface.rx_bytes - prev_interface.rx_bytes
|
||||
tx_bytes_diff = current_interface.tx_bytes - prev_interface.tx_bytes
|
||||
|
||||
# Calculate bytes per second
|
||||
rx_throughput = max(0, rx_bytes_diff / time_diff)
|
||||
tx_throughput = max(0, tx_bytes_diff / time_diff)
|
||||
|
||||
current_interface
|
||||
|> Map.put(:rx_throughput_bps, Float.round(rx_throughput, 2))
|
||||
|> Map.put(:tx_throughput_bps, Float.round(tx_throughput, 2))
|
||||
else
|
||||
# First measurement or time error
|
||||
current_interface
|
||||
|> Map.put(:rx_throughput_bps, 0.0)
|
||||
|> Map.put(:tx_throughput_bps, 0.0)
|
||||
end
|
||||
else
|
||||
# No previous data for this interface
|
||||
current_interface
|
||||
|> Map.put(:rx_throughput_bps, 0.0)
|
||||
|> Map.put(:tx_throughput_bps, 0.0)
|
||||
end
|
||||
_ ->
|
||||
# No previous data at all
|
||||
current_interface
|
||||
|> Map.put(:rx_throughput_bps, 0.0)
|
||||
|> Map.put(:tx_throughput_bps, 0.0)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user