{ pkgs, lib, config, ... }: let enabled = config.mod.disk-smart.enable; disks = [ { path = "/dev/disk/by-id/ata-ST8000VN004-3CP101_WWZ8QCG4"; name = "seagate_8tb_1"; label = "Seagate 8TB #1"; } { path = "/dev/disk/by-id/ata-ST8000VN004-3CP101_WWZ8QDJ5"; name = "seagate_8tb_2"; label = "Seagate 8TB #2"; } { path = "/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_85K2A0UCF4MJ"; name = "toshiba_20tb_1"; label = "Toshiba 20TB #1"; } { path = "/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_85K2A0V6F4MJ"; name = "toshiba_20tb_2"; label = "Toshiba 20TB #2"; } ]; outputDir = "/var/lib/disk-smart"; collectScript = pkgs.writeShellScript "disk-smart-collect" '' set -euo pipefail export PATH="${lib.makeBinPath [ pkgs.smartmontools pkgs.jq pkgs.coreutils ]}" mkdir -p ${outputDir} result="{" ${lib.concatMapStringsSep "\n" (disk: '' raw=$(smartctl -j -A -H ${disk.path} 2>/dev/null || true) temp=$(echo "$raw" | jq -r '.temperature.current // empty') power_on=$(echo "$raw" | jq -r '.power_on_time.hours // empty') smart_status=$(echo "$raw" | jq -r '.smart_status.passed // empty') reallocated=$(echo "$raw" | jq -r '[.ata_smart_attributes.table[] | select(.name == "Reallocated_Sector_Ct")][0].raw.value // empty') pending=$(echo "$raw" | jq -r '[.ata_smart_attributes.table[] | select(.name == "Current_Pending_Sector")][0].raw.value // empty') result="$result\"${disk.name}\":{\"temperature\":$temp,\"power_on_hours\":$power_on,\"smart_passed\":$smart_status,\"reallocated_sectors\":$reallocated,\"pending_sectors\":$pending}," '') disks} # Remove trailing comma, close object result="''${result%,}}" echo "$result" | jq . > ${outputDir}/smart.json.tmp mv ${outputDir}/smart.json.tmp ${outputDir}/smart.json ''; indent = prefix: s: lib.concatMapStringsSep "\n" (line: if line == "" then line else prefix + line) (lib.splitString "\n" s); mkSensor = disk: '' - name: "${disk.label} Temperature" value_template: "{{ value_json.${disk.name}.temperature }}" unit_of_measurement: "°C" device_class: temperature state_class: measurement - name: "${disk.label} Power On Hours" value_template: "{{ value_json.${disk.name}.power_on_hours }}" unit_of_measurement: "h" state_class: total_increasing - name: "${disk.label} SMART Passed" value_template: "{{ value_json.${disk.name}.smart_passed }}" - name: "${disk.label} Reallocated Sectors" value_template: "{{ value_json.${disk.name}.reallocated_sectors }}" state_class: measurement - name: "${disk.label} Pending Sectors" value_template: "{{ value_json.${disk.name}.pending_sectors }}" state_class: measurement ''; sensorYaml = indent " " (lib.concatMapStrings mkSensor disks); sectorEntities = lib.concatMap (disk: [ "sensor.${disk.name}_reallocated_sectors" "sensor.${disk.name}_pending_sectors" ]) disks; sectorEntitiesYaml = lib.concatMapStringsSep "\n" (id: " - ${id}") sectorEntities; smartPassedEntities = map (disk: "sensor.${disk.name}_smart_passed") disks; smartPassedEntitiesYaml = lib.concatMapStringsSep "\n" (id: " - ${id}") smartPassedEntities; in { options = { mod.disk-smart = { enable = lib.mkEnableOption "Enable disk SMART monitoring module"; }; }; config = lib.mkIf enabled { mod.home-assistant.extraConfig = '' rest: - resource: http://127.0.0.1:9633/smart.json scan_interval: 60 sensor: ${sensorYaml} automation disk_smart: - alias: "Disk sector count increased" trigger: - platform: state entity_id: ${sectorEntitiesYaml} condition: - condition: template value_template: "{{ trigger.from_state.state | int(-1) >= 0 and trigger.to_state.state | int(0) > trigger.from_state.state | int(0) }}" action: - service: notify.mobile_app_pixel_9_pro data: title: "Disk SMART warning" message: "{{ trigger.to_state.attributes.friendly_name }} increased from {{ trigger.from_state.state }} to {{ trigger.to_state.state }}" - alias: "Disk SMART check failed" trigger: - platform: state entity_id: ${smartPassedEntitiesYaml} condition: - condition: template value_template: "{{ trigger.to_state.state | lower == 'false' }}" action: - service: notify.mobile_app_pixel_9_pro data: title: "Disk SMART FAILURE" message: "{{ trigger.to_state.attributes.friendly_name }} reports SMART failure — drive is likely failing" ''; systemd.services.disk-smart-collect = { description = "Collect disk SMART data"; serviceConfig = { Type = "oneshot"; ExecStart = collectScript; }; }; systemd.timers.disk-smart-collect = { description = "Periodically collect disk SMART data"; wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "1min"; OnUnitActiveSec = "1min"; }; }; services.nginx.virtualHosts."127.0.0.1" = { listen = [ { addr = "127.0.0.1"; port = 9633; } ]; locations."= /smart.json" = { alias = "${outputDir}/smart.json"; extraConfig = '' default_type application/json; ''; }; }; }; }