Western Digital My Cloud

You’d think, wouldn’t you? Except that WD have thought of that, and - in a frenzy of XML standard breaking - have crippled configuration of locked down the cron as well.

However I have come up with an alternative trick, which avoids the need for ssh, but does need some minor manual intervention at each reboot.

  1. Logon as the ssh user.
  2. mkdir /mnt/HD/HD_a2/Nas_Prog/stats
  3. ln -s /mnt/HD/HD_a2/Nas_Prog/stats /var/www/stats

So

  • That’s the ‘stats’ folder now available at http://url_of_nas/stats.
  • The ‘stats’ folder and it’s contents are persistent, but you have to redo step 3 at every reboot. A pain, but substantially easier than dealing with the intracacies of ssh.

Next we need some content, preferrably serving JSON. The following are a version of the ssh files that came before:

.htaccess:

php_flag apc.cache_by_default Off

Disabling the apc cache is essential, unless you want the same stats each time you query.
UPDATE: The .htaccess file is not required for OS5.

disk.php

<?php
  $rtn = array();

  $cmd = "df | sed -n '/HD_a2/s/ \+/ /gp' | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $disktotres, $int);
  $rtn["disk"]["kB"]["total"] = $disktotres[0];

  $cmd = "df | sed -n '/HD_a2/s/ \+/ /gp' | cut -d' ' -f 3";
  // Execute the command, and get the result from the returned array
  exec($cmd, $diskusedres, $int);
  $rtn["disk"]["kB"]["used"] = $diskusedres[0];

  $cmd = "df | sed -n '/HD_a2/s/ \+/ /gp' | cut -d' ' -f 4";
  // Execute the command, and get the result from the returned array
  exec($cmd, $diskfreeres, $int);
  $rtn["disk"]["kB"]["free"] = $diskfreeres[0];

  $cmd = "df | sed -n '/HD_a2/s/ \+/ /gp' | cut -d' ' -f 5";
  // Execute the command, and get the result from the returned array
  exec($cmd, $diskpusedres, $int);
  $pctused = str_replace('%','',$diskpusedres[0]);
  $rtn["disk"]["percent"]["used"] = str_replace('%','',$pctused);
  $rtn["disk"]["percent"]["free"] = 100 - $pctused;

  // Return to the client
  echo json_encode($rtn, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
  // {"disk":{"kB":{"total":3837326424,"used":744981880,"free":3053341972},"percent":{"used":20,"free":80}}}
?>

memory.php

<?php
  $rtn = array();

  $cmd = "cat /proc/meminfo | sed -n '/MemTotal:/s/ \+/ /gp' | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $memtotres, $int);
  $rtn["memory"]["total"] = $memtotres[0];

  $cmd = "cat /proc/meminfo | sed -n '/MemFree:/s/ \+/ /gp' | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $memfreeres, $int);
  $rtn["memory"]["free"] = $memfreeres[0];

  $cmd = "cat /proc/meminfo | sed -n '/Buffers:/s/ \+/ /gp' | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $membufres, $int);
  $rtn["memory"]["buffered"] = $membufres[0];

  $cmd = "cat /proc/meminfo | sed -n '/Cached:/s/ \+/ /gp' | head -n 1 | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $memcachres, $int);
  $rtn["memory"]["cached"] = $memcachres[0];

  $cmd = "free -m | sed -n '/Swap/s/ \+/ /gp' | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $swaptotres, $int);
  $rtn["swap"]["total"] = $swaptotres[0];

  $cmd = "free -m | sed -n '/Swap/s/ \+/ /gp' | cut -d' ' -f 4";
  // Execute the command, and get the result from the returned array
  exec($cmd, $swapfreeres, $int);
  $rtn["swap"]["free"] = $swapfreeres[0];

  $cmd = "free -m | sed -n '/Swap/s/ \+/ /gp' | cut -d' ' -f 3";
  // Execute the command, and get the result from the returned array
  exec($cmd, $swapusedres, $int);
  $rtn["swap"]["used"] = $swapusedres[0];

  $cmd = "cat /proc/meminfo | sed -n '/SwapCached/s/ \+/ /gp' | cut -d' ' -f 2";
  // Execute the command, and get the result from the returned array
  exec($cmd, $swapcachres, $int);
  $rtn["swap"]["cached"] = $swapcachres[0];

  // Return to the client
  echo json_encode($rtn, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
  // {"memory":{"total":2084352,"free":907360,"buffered":165088,"cached":554944},"swap":{"total":2097056,"free":2097056,"used":0,"cached":0}}
?>

nas.php

<?php
  $rtn = array();

  $cmd = "/usr/sbin/fan_control -g 0";
  // Execute the command, and get the result from the returned array
  exec($cmd, $fanres, $int);
  //$rtn["res"] = $fanres;
  $rtn["temp"]["board"] = explode(" ", $fanres[0])[3];
  $rtn["temp"]["hd0"] = explode("=", $fanres[1])[1];
  $rtn["temp"]["hd1"] = explode("=", $fanres[2])[1];
  $rtn["temp"]["hd2"] = explode("=", $fanres[3])[1];
  $rtn["temp"]["hd3"] = explode("=", $fanres[4])[1];
  $rtn["temp"]["cpu"] = explode("=", $fanres[5])[1];

  $cmd = "/usr/local/sbin/getSmartStatus.sh";
  // Execute the command, and get the result from the returned array
  exec($cmd, $smtres, $int);
  $rtn["smart"] = $smtres[0];


  // Get the old software version
  $cmd = "cat /etc/NAS_CFG/config.xml | tr -d '\n' | tr -d '\t'";
  exec($cmd, $confxmlread, $int);
  $confxmlfile = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>'.$confxmlread[0];
  // Replace invalid XML tags <1> through to <5>
  for ($cnt = 1; $cnt < 6; $cnt++){
    $confxmlfile = str_replace("<".$cnt.">", "<cron_".$cnt.">", $confxmlfile);
    $confxmlfile = str_replace("</".$cnt.">", "</cron_".$cnt.">", $confxmlfile);
  }
  $confxmlres = simplexml_load_string($confxmlfile);
  $confxmlarr = get_object_vars($confxmlres);
  // Get the current firmware version
  $rtn["fw"]["current"] = $confxmlarr["sw_ver_1"];


  // Get the new software version
  $cmd = "cat /tmp/upgrade_count";
  exec($cmd, $newfw, $int);
  $newfw = $newfw[0];
  // Set a default
  $rtn["fw"]["new"] = $newfw;


  // Work out if it's the latest version
  if ($rtn["fw"]["current"] <> $rtn["fw"]["new"]) {
    $rtn["fw"]["status"] = "Upgrade to ".$rtn["fw"]["new"];
  }


  // CPU usage
  $cmd = "/usr/sbin/mpstat | sed -n '/all/s/ \+/ /gp' | cut -d' ' -f 12 | sed 's/%//g'";
  // Execute the command, and get the result from the returned array
  exec($cmd, $mpres, $int);
  $rtn["cpu"]["idle"] = $mpres[0];
  $rtn["cpu"]["used"] = 100 - $mpres[0];

  // $cmd = "fan_control -g 4 | cut -d' ' -f 4";
  $cmd = "cat /tmp/fan_rpm";
  // Execute the command, and get the result from the returned array
  exec($cmd, $fanrpmres, $int);
  $rtn["fan"]["rpm"] = $fanrpmres[0];

  // Return to the client
  echo json_encode($rtn, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
?>

storage.php

<?php
  $rtn = array();

  $cmd = "cat /tmp/storage_usage.xml";
  exec($cmd, $storagexml, $int);

  $storageres = simplexml_load_string($storagexml[0]);
  $storagearr = get_object_vars($storageres);
  $rtn["storage"]["unit"] = 'kB';
  $rtn["storage"]["size"] = $storagearr['size'] / 1024;
  $rtn["storage"]["usage"] = $storagearr['usage'] / 1024;
  $rtn["storage"]["video"] = $storagearr['video'] / 1024;
  $rtn["storage"]["photos"] = $storagearr['photos'] / 1024;
  $rtn["storage"]["music"] = $storagearr['music'] / 1024;
  $rtn["storage"]["other"] = $storagearr['other'] / 1024;

  // Return to the client
  echo json_encode($rtn, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
?>

Now that’s out of the way, it’s over to the Home Assistant box. Here, Home Assistant is installed on top of the Raspberry Pi OS, so I regret that, if this does not work for you, you’ll have to find your own way using the tools that are available in HASS OS, or whatever other OS you have chosen to run.

Here crontab is used to call a script to call curl to get a set of JSON files to the Home Assistant box. This gets around the ridiculously small 255 character limit of the REST Sensor tool in Home Assistant.

Crontab of homeassistant user: Every two minutes, reach out to the NAS for stats.

*/2 * * * * /home/homeassistant/bin/nas_stats.sh

The files are copied to .tmp, then renamed, to avoid timing errors (copying a completed file is instantaneous, compared to the time to arrive over http, and the sensors in Home Assistant really don’t like incomplete json!).

/home/homeassistant/bin/nas_stats.sh

#!/bin/bash
mkdir -p /home/homeassistant/.homeassistant/files
cd /home/homeassistant/.homeassistant/files
# Get the disk usage of my_nas
curl http://url_of_nas/stats/disk.php > my_nas_disk.json.tmp
rm my_nas_disk.json
mv my_nas_disk.json.tmp my_nas_disk.json
# Get the memory & swap usage of my_nas
curl http://url_of_nas/stats/memory.php > my_nas_memory.json.tmp
rm my_nas_memory.json
mv my_nas_memory.json.tmp my_nas_memory.json
# Get the board sensors of my_nas
curl http://url_of_nas/stats/nas.php > my_nas_nas.json.tmp
rm my_nas_nas.json
mv my_nas_nas.json.tmp my_nas_nas.json
# Get the storage usage of my_nas
curl http://url_of_nas/stats/storage.php > my_nas_storage.json.tmp
rm my_nas_storage.json
mv my_nas_storage.json.tmp my_nas_storage.json

Finally, place this file in the packages area of your Home Assistant:

nas.yaml

# /config/packages/nas.yaml

###################################
############ NAS STATS ############
###################################
# A cron job runs curl, which gets a file from  http://url_of_nas/stats/<something>.php
#
sensor:
- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_cpu_temp
  value_template: "{{ value_json.temp.cpu }}"
  unit_of_measurement: '°C'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_board_temp
  value_template: "{{ value_json.temp.board }}"
  unit_of_measurement: '°C'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_hd0_temp
  value_template: "{{ value_json.temp.hd0 }}"
  unit_of_measurement: '°C'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_hd1_temp
  value_template: "{{ value_json.temp.hd1 }}"
  unit_of_measurement: '°C'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_hd2_temp
  value_template: "{{ value_json.temp.hd2 }}"
  unit_of_measurement: '°C'
  
- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_hd3_temp
  value_template: "{{ value_json.temp.hd3 }}"
  unit_of_measurement: '°C'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_smart_status
  value_template: "{{ value_json.smart }}"

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_firmware_current
  value_template: "{{ value_json.fw.current }}"

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_firmware_new
  value_template: "{{ value_json.fw.new }}"

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_firmware_status
  value_template: "{{ value_json.fw.status }}"

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_fan_rpm
  value_template: "{{ value_json.fan.rpm }}"
  unit_of_measurement: 'rpm'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_disk.json
  name: nas_disk_space_total
  value_template: "{{ value_json.disk.kB.total }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_disk.json
  name: nas_disk_space_used
  value_template: "{{ value_json.disk.kB.used }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_disk.json
  name: nas_disk_space_free
  value_template: "{{ value_json.disk.kB.free }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_disk.json
  name: nas_disk_perc_used
  value_template: "{{ value_json.disk.percent.used }}"
  unit_of_measurement: '%'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_disk.json
  name: nas_disk_perc_free
  value_template: "{{ value_json.disk.percent.free }}"
  unit_of_measurement: '%'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_cpu_idle
  value_template: "{{ value_json.cpu.idle }}"
  unit_of_measurement: '%'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_nas.json
  name: nas_cpu_used
  value_template: "{{ value_json.cpu.used }}"
  unit_of_measurement: '%'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_storage.json
  name: nas_storage_size
  value_template: "{{ value_json.storage.size }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_storage.json
  name: nas_storage_usage
  value_template: "{{ value_json.storage.usage }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_storage.json
  name: nas_storage_video
  value_template: "{{ value_json.storage.video }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_storage.json
  name: nas_storage_photos
  value_template: "{{ value_json.storage.photos }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_storage.json
  name: nas_storage_music
  value_template: "{{ value_json.storage.music }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_storage.json
  name: nas_storage_other
  value_template: "{{ value_json.storage.other }}"
  unit_of_measurement: 'kB'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_mem_tot
  value_template: "{{ value_json.memory.total }}"
  unit_of_measurement: 'bytes'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_mem_free
  value_template: "{{ value_json.memory.free }}"
  unit_of_measurement: 'bytes'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_mem_buffered
  value_template: "{{ value_json.memory.buffered }}"
  unit_of_measurement: 'bytes'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_mem_cached
  value_template: "{{ value_json.memory.cached }}"
  unit_of_measurement: 'bytes'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_swap_tot
  value_template: "{{ value_json.swap.total }}"
  unit_of_measurement: 'bytes'

- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_swap_free
  value_template: "{{ value_json.swap.free }}"
  unit_of_measurement: 'bytes'
  
- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_swap_used
  value_template: "{{ value_json.swap.used }}"
  unit_of_measurement: 'bytes'
  
- platform: file
  file_path: /home/homeassistant/.homeassistant/files/my_nas_memory.json
  name: nas_swap_cached
  value_template: "{{ value_json.swap.cached }}"
  unit_of_measurement: 'bytes'

###################################
############ NAS STATS ############
###################################
- platform: template
  sensors:
    nas_ram_perc_used:
      friendly_name: NAS RAM Usage
      value_template: "{{ ( 100 * (states('sensor.nas_mem_tot')|int - states('sensor.nas_mem_free')|int - states('sensor.nas_mem_buffered')|int - states('sensor.nas_mem_cached')|int ) / states('sensor.nas_mem_tot')|int) | round(1) }}"
      unit_of_measurement: '%'
    nas_swap_perc_used:
      friendly_name: NAS Swap Usage
      value_template: "{{ ((states('sensor.nas_swap_used')|int / states('sensor.nas_swap_tot')|int) * 100) | round(1) }}"
      unit_of_measurement: '%'

# Average Fan RPM
- platform: filter
  name: "nas_fan_rpm_smooth"
  entity_id: sensor.nas_fan_rpm
  filters:
    - filter: lowpass
      time_constant: 10
      precision: 2

Ok, I think that’s everything I have. I’m not claiming the stats are all correct - in fact at least some of them won’t be, my knowledge of the WD innards and Bash is enough to be dangerous, but certainly not in-depth - so I’m happy to stand corrected.