equipment_id, "sensor_type" => sensor_type} = params, _, socket) do
+ if connected?(socket) do
+ Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{equipment_id}")
+ end
+
+ range = Map.get(params, "range", "24h")
+
+ socket =
+ socket
+ |> assign(:equipment_id, equipment_id)
+ |> assign(:sensor_type, sensor_type)
+ |> assign(:range, range)
+ |> load_graph_data()
+
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("change_range", %{"range" => range}, socket) do
+ {:noreply,
+ push_patch(socket,
+ to:
+ ~p"/orgs/#{socket.assigns.current_organization.slug}/equipment/#{socket.assigns.equipment_id}/graph/#{socket.assigns.sensor_type}?range=#{range}"
+ )}
+ end
+
+ # Private functions
+
+ defp load_graph_data(socket) do
+ equipment_id = socket.assigns.equipment_id
+ sensor_type = socket.assigns.sensor_type
+ range = socket.assigns.range
+
+ equipment = Equipment.get_equipment!(equipment_id)
+ sensor_types = get_sensor_types_for_chart(sensor_type)
+ chart_data = load_sensor_chart_data(equipment_id, sensor_types, range)
+
+ {title, unit, auto_scale} = get_chart_config(sensor_type)
+
+ socket
+ |> assign(:equipment, equipment)
+ |> assign(:page_title, "#{equipment.name} - #{title}")
+ |> assign(:chart_title, title)
+ |> assign(:chart_data, chart_data)
+ |> assign(:unit, unit)
+ |> assign(:auto_scale, auto_scale)
+ end
+
+ defp get_sensor_types_for_chart("processors"), do: ["cpu_load"]
+ defp get_sensor_types_for_chart("memory"), do: ["memory_usage"]
+ defp get_sensor_types_for_chart("storage"), do: ["disk_usage"]
+ defp get_sensor_types_for_chart("temperature"), do: ["temperature", "cpu_temperature", "celsius"]
+ defp get_sensor_types_for_chart("voltage"), do: ["voltage", "volts"]
+ defp get_sensor_types_for_chart(_), do: []
+
+ defp get_chart_config("processors"), do: {"Processor Usage", "%", false}
+ defp get_chart_config("memory"), do: {"Memory Usage", "%", false}
+ defp get_chart_config("storage"), do: {"Storage Usage", "%", false}
+ defp get_chart_config("temperature"), do: {"Temperature", "°C", true}
+ defp get_chart_config("voltage"), do: {"Voltage", "V", true}
+ defp get_chart_config(_), do: {"Sensor Data", "", false}
+
+ defp load_sensor_chart_data(equipment_id, sensor_types, range) do
+ case Snmp.get_device_with_associations(equipment_id) do
+ nil ->
+ nil
+
+ device ->
+ # Find all sensors of the specified types
+ sensors =
+ device.sensors
+ |> Enum.filter(&(&1.sensor_type in sensor_types))
+ |> Enum.sort_by(& &1.sensor_index)
+
+ if Enum.empty?(sensors) do
+ nil
+ else
+ # Get readings based on range
+ since = get_datetime_from_range(range)
+
+ datasets =
+ Enum.map(sensors, fn sensor ->
+ readings =
+ sensor.id
+ |> Snmp.get_sensor_readings(
+ since: since,
+ limit: get_limit_for_range(range)
+ )
+ |> Enum.reverse()
+
+ %{
+ label: sensor.sensor_descr,
+ data:
+ Enum.map(readings, fn reading ->
+ %{
+ x: DateTime.to_unix(reading.checked_at, :millisecond),
+ y: if(reading.value, do: Float.round(reading.value, 1))
+ }
+ end)
+ }
+ end)
+
+ Jason.encode!(%{datasets: datasets})
+ end
+ end
+ end
+
+ defp get_datetime_from_range("1h"), do: DateTime.add(DateTime.utc_now(), -1, :hour)
+ defp get_datetime_from_range("6h"), do: DateTime.add(DateTime.utc_now(), -6, :hour)
+ defp get_datetime_from_range("12h"), do: DateTime.add(DateTime.utc_now(), -12, :hour)
+ defp get_datetime_from_range("24h"), do: DateTime.add(DateTime.utc_now(), -24, :hour)
+ defp get_datetime_from_range("7d"), do: DateTime.add(DateTime.utc_now(), -7, :day)
+ defp get_datetime_from_range("30d"), do: DateTime.add(DateTime.utc_now(), -30, :day)
+ defp get_datetime_from_range(_), do: DateTime.add(DateTime.utc_now(), -24, :hour)
+
+ defp get_limit_for_range("1h"), do: 360
+ defp get_limit_for_range("6h"), do: 720
+ defp get_limit_for_range("12h"), do: 1440
+ defp get_limit_for_range("24h"), do: 2880
+ defp get_limit_for_range("7d"), do: 10_080
+ defp get_limit_for_range("30d"), do: 43_200
+ defp get_limit_for_range(_), do: 2880
+end
diff --git a/lib/towerops_web/live/graph_live/show.html.heex b/lib/towerops_web/live/graph_live/show.html.heex
new file mode 100644
index 00000000..4e715477
--- /dev/null
+++ b/lib/towerops_web/live/graph_live/show.html.heex
@@ -0,0 +1,74 @@
+
+
+
+
+
+ <.link
+ navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment_id}"}
+ class="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
+ >
+ <.icon name="hero-arrow-left" class="h-5 w-5" />
+
+
+
{@chart_title}
+
{@equipment.name}
+
+
+
+
+
+
Time Range:
+
+ <%= for {label, value} <- [{"1 Hour", "1h"}, {"6 Hours", "6h"}, {"12 Hours", "12h"}, {"24 Hours", "24h"}, {"7 Days", "7d"}, {"30 Days", "30d"}] do %>
+
+ <% end %>
+
+
+
+ <%= if @chart_data do %>
+
+ <% else %>
+
+
+ <.icon name="hero-chart-bar" class="mx-auto h-12 w-12 text-zinc-400" />
+
+ No sensor data available
+
+
+ This device doesn't have any sensors of this type.
+
+
+
+ <% end %>
+
+
diff --git a/lib/towerops_web/router.ex b/lib/towerops_web/router.ex
index 737902ee..a2654696 100644
--- a/lib/towerops_web/router.ex
+++ b/lib/towerops_web/router.ex
@@ -109,6 +109,7 @@ defmodule ToweropsWeb.Router do
live "/equipment/new", EquipmentLive.Form, :new
live "/equipment/:id", EquipmentLive.Show, :show
live "/equipment/:id/edit", EquipmentLive.Form, :edit
+ live "/equipment/:id/graph/:sensor_type", GraphLive.Show, :show
# Alert routes
live "/alerts", AlertLive.Index, :index