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
|
# Mosquito for MQTT support
|
||||||
mosquitto
|
mosquitto
|
||||||
|
|
||||||
|
# Network monitoring
|
||||||
|
iftop
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
lib,
|
lib,
|
||||||
beamPackages,
|
beamPackages,
|
||||||
src,
|
src,
|
||||||
|
iftop,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
beamPackages.mixRelease rec {
|
beamPackages.mixRelease rec {
|
||||||
@ -10,6 +11,9 @@ beamPackages.mixRelease rec {
|
|||||||
|
|
||||||
inherit src;
|
inherit src;
|
||||||
|
|
||||||
|
# Runtime dependencies
|
||||||
|
buildInputs = [ iftop ];
|
||||||
|
|
||||||
# Disable distributed Erlang to avoid COOKIE requirement
|
# Disable distributed Erlang to avoid COOKIE requirement
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
# Create wrapper script that sets proper environment including COOKIE
|
# 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_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 }}")},
|
{"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
|
# Network Sensors - Primary interface throughput
|
||||||
{"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_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_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 }}")},
|
{"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 Sensors for status
|
||||||
{"binary_sensor", "system_online", build_binary_sensor_config("System Online", "#{base_topic}", "mdi:server", "connectivity")}
|
{"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
|
# Store both configs for later use
|
||||||
state_config = %{
|
state_config = %{
|
||||||
app_config: app_config,
|
app_config: app_config,
|
||||||
mqtt_config: mqtt_config
|
mqtt_config: mqtt_config,
|
||||||
|
previous_network_stats: nil
|
||||||
}
|
}
|
||||||
|
|
||||||
connection_opts = [
|
connection_opts = [
|
||||||
@ -64,14 +65,14 @@ defmodule Systant.MqttClient do
|
|||||||
|
|
||||||
def handle_info(:publish_startup_stats, state) do
|
def handle_info(:publish_startup_stats, state) do
|
||||||
Logger.info("Publishing initial system metrics")
|
Logger.info("Publishing initial system metrics")
|
||||||
publish_stats(state.app_config, state.mqtt_config)
|
{_stats, updated_state} = collect_and_publish_stats(state)
|
||||||
{:noreply, state}
|
{:noreply, updated_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:publish_stats, state) do
|
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)
|
schedule_stats_publish(state.mqtt_config.publish_interval)
|
||||||
{:noreply, state}
|
{:noreply, updated_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(_msg, state) do
|
def handle_info(_msg, state) do
|
||||||
@ -83,6 +84,36 @@ defmodule Systant.MqttClient do
|
|||||||
:ok
|
:ok
|
||||||
end
|
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
|
defp publish_stats(app_config, mqtt_config) do
|
||||||
stats = Systant.SystemMetrics.collect_metrics(app_config)
|
stats = Systant.SystemMetrics.collect_metrics(app_config)
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ defmodule Systant.SystemMetrics do
|
|||||||
@doc """
|
@doc """
|
||||||
Collect system metrics based on configuration
|
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()
|
config = config || Systant.Config.load_config()
|
||||||
|
|
||||||
base_metrics = %{
|
base_metrics = %{
|
||||||
@ -27,7 +27,7 @@ defmodule Systant.SystemMetrics do
|
|||||||
"memory" -> Map.put(acc, :memory, collect_memory_metrics(config))
|
"memory" -> Map.put(acc, :memory, collect_memory_metrics(config))
|
||||||
"disk" -> Map.put(acc, :disk, collect_disk_metrics(config))
|
"disk" -> Map.put(acc, :disk, collect_disk_metrics(config))
|
||||||
"gpu" -> Map.put(acc, :gpu, collect_gpu_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))
|
"temperature" -> Map.put(acc, :temperature, collect_temperature_metrics(config))
|
||||||
"processes" -> Map.put(acc, :processes, collect_process_metrics(config))
|
"processes" -> Map.put(acc, :processes, collect_process_metrics(config))
|
||||||
"system" -> Map.put(acc, :system, collect_system_info(config))
|
"system" -> Map.put(acc, :system, collect_system_info(config))
|
||||||
@ -73,10 +73,10 @@ defmodule Systant.SystemMetrics do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Collect network interface statistics
|
Collect network interface statistics with throughput calculation
|
||||||
"""
|
"""
|
||||||
def collect_network_metrics(config) do
|
def collect_network_metrics(config, previous_stats \\ nil) do
|
||||||
get_network_stats(config)
|
get_network_stats(config, previous_stats)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@ -681,19 +681,23 @@ defmodule Systant.SystemMetrics do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_network_stats(config) do
|
defp get_network_stats(config, previous_stats \\ nil) do
|
||||||
network_config = Systant.Config.get(config, ["network"]) || %{}
|
network_config = Systant.Config.get(config, ["network"]) || %{}
|
||||||
|
current_time = System.monotonic_time(:second)
|
||||||
|
|
||||||
try do
|
try do
|
||||||
case File.read("/proc/net/dev") do
|
case File.read("/proc/net/dev") do
|
||||||
{:ok, content} ->
|
{:ok, content} ->
|
||||||
content
|
current_interfaces = content
|
||||||
|> String.split("\n")
|
|> String.split("\n")
|
||||||
|> Enum.drop(2)
|
|> Enum.drop(2)
|
||||||
|> Enum.filter(&(String.trim(&1) != ""))
|
|> Enum.filter(&(String.trim(&1) != ""))
|
||||||
|> Enum.map(&parse_network_interface/1)
|
|> Enum.map(&parse_network_interface/1)
|
||||||
|> Enum.filter(&(&1 != nil))
|
|> Enum.filter(&(&1 != nil))
|
||||||
|> filter_network_interfaces(network_config)
|
|> filter_network_interfaces(network_config)
|
||||||
|
|> Enum.map(&calculate_throughput(&1, previous_stats, current_time))
|
||||||
|
|
||||||
|
current_interfaces
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
@ -794,4 +798,44 @@ defmodule Systant.SystemMetrics do
|
|||||||
end
|
end
|
||||||
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
|
end
|
||||||
Loading…
Reference in New Issue
Block a user