Apple Content Caching statistics in Home Assistant

I’ve always run an Apple Content Caching server at home because it is simple to setup and requires no maintenance, so why not!

With a house full of Apple devices we might as well take advantage of the functionality to speed up downloads.

I first started playing with Home Assistant because I wanted an easier interface to visualize my energy usage without logging into Smart Meter Texas.

With built-in integrations for UniFi networking and Pi-hole, a networking dashboard was the next thing on my list to tackle. There isn’t a built-in integration for Apple Content Caching, however with a custom sensor and a LaunchDaemon it is easy to get the data reporting in and visualized.

Read on for the details…

The Approach

I went down a few rabbit holes and dismissed some options like installing a binary via homebrew when I came across a post by “zacs” on the Home Assistant community forums from 2021.

No external dependencies. Easy to implement. This is exactly what I was looking for!

zacs’ approach is simple:

  • A custom webhook sensor template in Home Assistant
  • AssetCacheManagerUtil status output in JSON format
  • A LaunchDaemon to run the command on schedule and use curl to push it to the Home Assistant webhook

The Sensor

I took zacs’ example and modified it with a few changes.

  1. I want to make sure I am capturing all output that AssetCacheManagerUtil provides, so I added additional sensors.
  2. I want to see the data in GB, not MB, and have it match exactly what you see when running AssetCacheManagerUtil status on the CLI in human readable format.

Below is the resulting YAML to add to Home Assistant’s templates.yaml file:


- trigger:
    - platform: webhook
      webhook_id: apple_cache
  sensor:
    # Main cache usage
    - name: "Apple Cache: Actual Cache Used"
      state: "{{ (trigger.json.result.ActualCacheUsed | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache Free"
      state: "{{ (trigger.json.result.CacheFree | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache Used"
      state: "{{ (trigger.json.result.CacheUsed | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache Limit"
      state: "{{ (trigger.json.result.CacheLimit | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"

    # CacheDetails breakdown
    - name: "Apple Cache: Cache (iCloud)"
      state: "{{ (trigger.json.result.CacheDetails.iCloud | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache (iOS Software)"
      state: "{{ (trigger.json.result.CacheDetails['iOS Software'] | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache (Mac Software)"
      state: "{{ (trigger.json.result.CacheDetails['Mac Software'] | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache (Movies)"
      state: "{{ (trigger.json.result.CacheDetails.Movies | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Cache (Other)"
      state: "{{ (trigger.json.result.CacheDetails.Other | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"

    # Max pressure
    - name: "Apple Cache: Maximum Pressure (past hour)"
      state: "{{ trigger.json.result.MaxCachePressureLast1Hour }}"
      unit_of_measurement: "%"

    # Personal cache
    - name: "Apple Cache: Personal Cache Free"
      state: "{{ (trigger.json.result.PersonalCacheFree | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Personal Cache Limit"
      state: "{{ (trigger.json.result.PersonalCacheLimit | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Personal Cache Used"
      state: "{{ (trigger.json.result.PersonalCacheUsed | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"

    # TotalBytes metrics
    - name: "Apple Cache: Stored From Origin"
      state: "{{ (trigger.json.result.TotalBytesStoredFromOrigin | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Stored From Parents"
      state: "{{ (trigger.json.result.TotalBytesStoredFromParents | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Stored From Peers"
      state: "{{ (trigger.json.result.TotalBytesStoredFromPeers | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Dropped"
      state: "{{ (trigger.json.result.TotalBytesDropped | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Served to Clients"
      state: "{{ (trigger.json.result.TotalBytesReturnedToClients | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Returned to Children"
      state: "{{ (trigger.json.result.TotalBytesReturnedToChildren | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Returned to Peers"
      state: "{{ (trigger.json.result.TotalBytesReturnedToPeers | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"
    - name: "Apple Cache: Imported"
      state: "{{ (trigger.json.result.TotalBytesImported | int / 1000000000) | round(2) }}"
      unit_of_measurement: "GB"

  binary_sensor:
    - name: "Apple Cache Active"
      state: "{{ trigger.json.result.Active }}"
    - name: "Apple Cache Activated"
      state: "{{ trigger.json.result.Activated }}"

The LaunchDaemon

Next we need a LaunchDaemon to run on the Mac hosting the content cache that will grab the data on a schedule and push it to Home Assistant.

Again a few tweaks were needed from the old post to add logging and I decided on a five minute interval to push data.

Below is an example LaunchDaemon that can used on your content caching server:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>com.example.applecache</string>
	<key>StartInterval</key>
	<integer>300</integer>
	<key>RunAtLoad</key>
	<true/>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/bash</string>
		<string>-c</string>
		<string>/usr/bin/AssetCacheManagerUtil status -j | /usr/bin/curl -f -s -H "Content-Type: application/json" -X POST --data-binary @- http://192.168.1.1:8123/api/webhook/apple_cache && echo "$(date) Apple Cache POST OK"</string>
	</array>
	<key>StandardOutPath</key>
	<string>/var/log/apple_cache_ha.log</string>
	<key>StandardErrorPath</key>
	<string>/var/log/apple_cache_ha.err</string>
</dict>
</plist>

The Dashboard

Lastly we need to display the data in Home Assistant. I decided to add a small section with the most relevant tiles on my Networking dashboard, but I also to created a full dashboard dedicated to see everything related to the content cache.

The YAML for the full dashboard is below:


views:
  - title: Apple Content Caching
    path: apple_cache
    badges: []
    cards:
      - type: gauge
        entity: sensor.apple_cache_actual_cache_used
        name: 'Apple Cache: Actual Cache Used'
        unit: GB
        min: 0
        max: 150
        severity:
          green: 0
          yellow: 100
          red: 130
      - type: gauge
        entity: sensor.apple_cache_cache_free
        name: 'Apple Cache: Cache Free'
        unit: GB
        min: 0
        max: 150
        severity:
          green: 100
          yellow: 50
          red: 0
      - type: gauge
        entity: sensor.apple_cache_cache_used
        name: 'Apple Cache: Cache Used'
        unit: GB
        min: 0
        max: 150
        severity:
          green: 0
          yellow: 100
          red: 130
      - type: gauge
        entity: sensor.apple_cache_maximum_pressure_past_hour
        name: 'Apple Cache: Max Pressure (1h)'
        unit: '%'
        min: 0
        max: 100
        severity:
          green: 0
          yellow: 50
          red: 80
      - type: entities
        title: Apple Cache Details
        show_header_toggle: false
        state_color: true
        entities:
          - entity: binary_sensor.apple_cache_activated
            name: 'Apple Cache: Activated'
          - entity: binary_sensor.apple_cache_active
            name: 'Apple Cache: Active'
          - entity: sensor.apple_cache_cache_icloud
            name: 'Apple Cache: Cache (iCloud)'
          - entity: sensor.apple_cache_cache_ios_software
            name: 'Apple Cache: Cache (iOS Software)'
          - entity: sensor.apple_cache_cache_mac_software
            name: 'Apple Cache: Cache (Mac Software)'
          - entity: sensor.apple_cache_cache_movies
            name: 'Apple Cache: Cache (Movies)'
          - entity: sensor.apple_cache_cache_other
            name: 'Apple Cache: Cache (Other)'
          - entity: sensor.apple_cache_personal_cache_free
            name: 'Apple Cache: Personal Cache Free'
          - entity: sensor.apple_cache_personal_cache_limit
            name: 'Apple Cache: Personal Cache Limit'
          - entity: sensor.apple_cache_personal_cache_used
            name: 'Apple Cache: Personal Cache Used'
          - entity: sensor.apple_cache_stored_from_origin
            name: 'Apple Cache: Stored From Origin'
          - entity: sensor.apple_cache_stored_from_parents
            name: 'Apple Cache: Stored From Parents'
          - entity: sensor.apple_cache_stored_from_peers
            name: 'Apple Cache: Stored From Peers'
          - entity: sensor.apple_cache_served_to_clients
            name: 'Apple Cache: Served to Clients'
          - entity: sensor.apple_cache_returned_to_children
            name: 'Apple Cache: Returned to Children'
          - entity: sensor.apple_cache_returned_to_peers
            name: 'Apple Cache: Returned to Peers'
          - entity: sensor.apple_cache_dropped
            name: 'Apple Cache: Dropped'
          - entity: sensor.apple_cache_imported
            name: 'Apple Cache: Imported'
      - type: history-graph
        title: 'Trends: Cache Used'
        entities:
          - sensor.apple_cache_cache_used
        hours_to_show: 168
        refresh_interval: 5
      - type: history-graph
        title: 'Trends: Cache Pressure'
        entities:
          - sensor.apple_cache_maximum_pressure_past_hour
        hours_to_show: 168
        refresh_interval: 5
    type: masonry

The Result

The result are some easy to read charts that let me keep on eye on how effective the Apple Content Caching is with minimal effort. I’m pretty happy with the results:


Leave a Reply

Your email address will not be published. Required fields are marked *