---
name: Home Command Center
slug: home-command-center
category: Automation
description: Home Command Center controls a Homey Pro hub from Claude Code. Use it to query devices, flows, energy, presence, and alarms, or to run home automation actions with local or cloud fallback.
github: "https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-home"
language: Shell
stars: 186
forks: 21
install: "npx degit https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-home ~/.claude/skills/ops-home"
installs_to: ~/.claude/skills/ops-home
source_path: claude-ops/skills/ops-home/SKILL.md
collection_size: 25
category_size: 2032
collection_url: "https://dirskills.com/collections/Lifecycle-Innovations-Limited/claude-ops"
added: 2026-09-06T05:20:38.768Z
last_synced: 2026-09-06T05:20:38.768Z
canonical_url: "https://dirskills.com/skills/home-command-center"
---

# Home Command Center

Home Command Center controls a Homey Pro hub from Claude Code. Use it to query devices, flows, energy, presence, and alarms, or to run home automation actions with local or cloud fallback.

**Install:**

```bash
npx degit https://github.com/Lifecycle-Innovations-Limited/claude-ops/tree/main/claude-ops/skills/ops-home ~/.claude/skills/ops-home
```

## README

# OPS ► HOME — Smart Home Command Center (Homey Pro)

Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Workflow` are missing, follow Rule 10 in `ops-rules` (Hermes: numbered options / two-turn Telegram card; `delegate_task`).

## Runtime Context

Before executing, load available context:

1. **Preferences**: Read `${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json`
   - `timezone` — display all timestamps in user's timezone
   - `home_automation.homey_local_url` — e.g. `http://192.168.1.100` (preferred path, faster, no cloud dependency)
   - `home_automation.homey_local_token` — Personal Access Token for local API
   - `home_automation.homey_cloud_token` — Athom OAuth token for cloud fallback
   - `home_automation.homey_id` — Homey ID (cloud resolution)

2. **Daemon health**: Read `${CLAUDE_PLUGIN_DATA_DIR}/daemon-health.json`
   - If `action_needed` is not null → surface it before running any Homey operations
   - On any auth/connectivity failure in this skill, write `action_needed` back to daemon-health.json

3. **Secrets**: Resolve Homey credentials via userConfig → env vars → Doppler → keychain (see Phase 1 below)

## Agent Teams support

If `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` is set, use **Agent Teams** for parallel hub probing:

```
TeamCreate("home-team")
Agent(team_name="home-team", name="devices-scanner", prompt="List all Homey devices, group by zone, return online/offline state and current capabilities")
Agent(team_name="home-team", name="flows-scanner", prompt="List all flows and their last-fired timestamps")
Agent(team_name="home-team", name="energy-scanner", prompt="Pull live power draw + today's kWh + top consumers, flag anomalies vs 7-day baseline")
Agent(team_name="home-team", name="presence-scanner", prompt="Return presence state and active alarms")
```

If the flag is NOT set, dispatch `ops:home-agent` as standard fire-and-forget subagents per scope (devices, flows, energy, presence, alarms).

## Phase 1 — Resolve credentials

Resolve Homey credentials in this order. Local path is preferred (faster, works offline, lower latency):

```bash
PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"

# 1. Plugin userConfig (preferences.json)
HOMEY_LOCAL_URL=$(jq -r '.home_automation.homey_local_url // empty' "$PREFS_PATH" 2>/dev/null)
HOMEY_LOCAL_TOKEN=$(jq -r '.home_automation.homey_local_token // empty' "$PREFS_PATH" 2>/dev/null)
HOMEY_CLOUD_TOKEN=$(jq -r '.home_automation.homey_cloud_token // empty' "$PREFS_PATH" 2>/dev/null)
HOMEY_ID=$(jq -r '.home_automation.homey_id // empty' "$PREFS_PATH" 2>/dev/null)

# 2. Environment variables (override userConfig if set)
[ -n "$HOMEY_LOCAL_URL" ] || HOMEY_LOCAL_URL="${HOMEY_LOCAL_URL:-}"
[ -n "$HOMEY_LOCAL_TOKEN" ] || HOMEY_LOCAL_TOKEN="${HOMEY_LOCAL_TOKEN:-}"
[ -n "$HOMEY_CLOUD_TOKEN" ] || HOMEY_CLOUD_TOKEN="${HOMEY_CLOUD_TOKEN:-${HOMEY_ACCESS_TOKEN:-}}"
[ -n "$HOMEY_ID" ] || HOMEY_ID="${HOMEY_ID:-}"

# 3. Doppler fallback (project: homey-pro)
if [ -z "$HOMEY_LOCAL_TOKEN" ] && command -v doppler &>/dev/null; then
  HOMEY_LOCAL_TOKEN=$(doppler secrets get HOMEY_LOCAL_TOKEN --project homey-pro --plain 2>/dev/null)
fi
if [ -z "$HOMEY_CLOUD_TOKEN" ] && command -v doppler &>/dev/null; then
  HOMEY_CLOUD_TOKEN=$(doppler secrets get HOMEY_ACCESS_TOKEN --project homey-pro --plain 2>/dev/null)
fi
if [ -z "$HOMEY_LOCAL_URL" ] && command -v doppler &>/dev/null; then
  HOMEY_LOCAL_URL=$(doppler secrets get HOMEY_LOCAL_URL --project homey-pro --plain 2>/dev/null)
fi
if [ -z "$HOMEY_ID" ] && command -v doppler &>/dev/null; then
  HOMEY_ID=$(doppler secrets get HOMEY_ID --project homey-pro --plain 2>/dev/null)
fi

# 4. Keychain fallback
[ -z "$HOMEY_LOCAL_TOKEN" ] && HOMEY_LOCAL_TOKEN=$(security find-generic-password -s "homey-local-token" -w 2>/dev/null)
[ -z "$HOMEY_CLOUD_TOKEN" ] && HOMEY_CLOUD_TOKEN=$(security find-generic-password -s "homey-cloud-token" -w 2>/dev/null)
```

If neither a (`HOMEY_LOCAL_URL` + `HOMEY_LOCAL_TOKEN`) pair nor (`HOMEY_CLOUD_TOKEN` + `HOMEY_ID`) is resolvable, tell the user and exit gracefully:

```
No Homey credentials configured. Run /ops:setup --section home to configure your Homey Pro hub.
```

Write `action_needed: "configure_homey"` to `daemon-health.json` and exit.

Set helpers used by all phases below:

```bash
HOMEY_BASE_LOCAL="${HOMEY_LOCAL_URL}/api/manager"
HOMEY_BASE_CLOUD="https://api.athom.com/v2/homey/${HOMEY_ID}"
HOMEY_AUTH_LOCAL="Authorization: Bearer ${HOMEY_LOCAL_TOKEN}"
HOMEY_AUTH_CLOUD="Authorization: Bearer ${HOMEY_CLOUD_TOKEN}"

# homey_call <path> [method] [body] — tries local first, falls back to cloud on failure
homey_call() {
  local path="$1" method="${2:-GET}" body="${3:-}"
  local resp http_code
  if [ -n "$HOMEY_LOCAL_URL" ] && [ -n "$HOMEY_LOCAL_TOKEN" ]; then
    resp=$(curl -s -o /tmp/homey_resp -w "%{http_code}" -X "$method" \
      -H "$HOMEY_AUTH_LOCAL" -H "Content-Type: application/json" \
      ${body:+--data "$body"} \
      "${HOMEY_BASE_LOCAL}${path}" 2>/dev/null)
    if [ "$resp" -ge 200 ] && [ "$resp" -lt 300 ]; then
      cat /tmp/homey_resp; return 0
    fi
  fi
  # Fallback to cloud
  if [ -n "$HOMEY_CLOUD_TOKEN" ] && [ -n "$HOMEY_ID" ]; then
    # Map manager-style local paths to cloud equivalents where possible
    local cloud_path="${path/\/devices\/device/\/devices}"
    cloud_path="${cloud_path/\/flow\/flow/\/flows}"
    cloud_path="${cloud_path/\/zones\/zone/\/zones}"
    curl -s -X "$method" \
      -H "$HOMEY_AUTH_CLOUD" -H "Content-Type: application/json" \
      ${body:+--data "$body"} \
      "${HOMEY_BASE_CLOUD}${cloud_path}"
    return $?
  fi
  echo '{"error":"no homey transport available"}'
  return 1
}
```

---

## Phase 2 — Route by $ARGUMENTS

| Input                                   | Action                |
| --------------------------------------- | --------------------- |
| (empty)                                 | Home status dashboard |
| status, dashboard                       | Home status dashboard |
| devices, device, lights, locks, sensors | Devices manager       |
| flow, flows                             | Trigger / list flows  |
| scene, scenes                           | Trigger scene (alias) |
| energy, power, kwh                      | Energy dashboard      |
| climate, temp, thermostat, heating      | Climate manager       |
| presence, who, home                     | Presence              |
| alarm, alarms, arm, disarm, security    | Alarms / security     |
| health, diagnose, outage                | Health / outage scan  |
| sonos, speaker, music, play, volume, group | Sonos audio control   |
| setup, configure, init, token           | Setup flow            |

---

## STATUS (default — empty $ARGUMENTS)

One-screen home dashboard. Run devices/flows/energy/presence/alarms calls in parallel (separate Bash calls or Agent Team), then render.

```bash
# Devices online state
homey_call "/devices/device" | jq '{
  total: (. | length),
  online: ([.[] | select(.available == true)] | length),
  offline: ([.[] | select(.available == false)] | length)
}'

# Flows
homey_call "/flow/flow" | jq '{
  total: (. | length),
  enabled: ([.[] | select(.enabled == true)] | length),
  last_fired: ([.[] | select(.lastExecuted != null)] | sort_by(.lastExecuted) | reverse | .[0] | {name, lastExecuted})
}'

# Live energy
homey_call "/energy/live" | jq '{watts: .totalPower // 0}'

# Presence
homey_call "/presence" | jq '[.[] | select(.present == true) | .name]'

# Alarms (active)
homey_call "/alarms/alarm" | jq '[.[] | select(.active == true)]'

# Zones
homey_call "/zones/zone" | jq '[.[] | .name]'
```

Desktop render:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME — [hub-name] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HUB          [name]  ([firmware version])
TRANSPORT    local | cloud  ([latency-ms]ms)

DEVICES      [N online] / [N total]   [N offline]
FLOWS        [N enabled] / [N total]
LAST FLOW    [name] fired [N min] ago

POWER        [W] now    [kWh] today
ZONES        [N rooms]
PRESENCE     [name1], [name2]   — home

ALARMS       [N active]
  [type] in [zone] — [device]    (if active)

──────────────────────────────────────────────────────
 [d] devices   [f] flows   [e] energy
 [c] climate   [p] presence   [a] alarms
 /ops:ops-home setup — configure credentials
──────────────────────────────────────────────────────
```

Mobile mode (`$SSH_CONNECTION` set or `$OPS_MOBILE=1`): plain text only, 5–8 lines max, no banners.

```
home: [N]/[N] devices · [W]W now · [kWh] today.
flows: [N] enabled · last "[name]" [N]m ago.
presence: [names] home.
alarms: [N] active.
next: /ops-home devices | flows | energy
```

If `[N] alarms active` is non-zero AND any are critical (smoke/leak/security), surface immediately at top of output and suggest piping to `/ops:ops-comms` to broadcast.

Use `AskUserQuestion` for next-action selection (max 4 options per Rule 1).

---

## DEVICES

List devices, group by zone. Support filter and bulk-toggle.

```bash
# All devices with zone + capabilities
homey_call "/devices/device" | jq '[.[] | {
  id: .id,
  name: .name,
  zone: .zoneName,
  driverId: .driverId,
  class: .class,
  available: .available,
  capabilities: .capabilities,
  capabilityValues: (.capabilitiesObj // {} | to_entries | map({key: .key, value: .value.value}))
}]'

# Zones for grouping
homey_call "/zones/zone" | jq '[.[] | {id: .id, name: .name}]'
```

Apply filter from `$ARGUMENTS`:

- `devices lights` → filter `class == "light"` or capability includes `dim`/`light_hue`
- `devices locks` → filter `class == "lock"` or capability includes `locked`
- `devices climate` → filter capability includes `target_temperature` or `measure_temperature`
- `devices sensors` → filter capability starts with `measure_` or `alarm_`
- `devices --off` → set `onoff=false` on filtered set (REQUIRES `AskUserQuestion` confirmation — destructive)
- `devices --on` → set `onoff=true` on filtered set (REQUIRES `AskUserQuestion` confirmation)

Render grouped by zone:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME ► DEVICES — [filter] — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[ZONE: Living Room]
  [Living Room Lamp]      light    on    dim:0.65    online
  [Sofa Outlet]           socket   off                online
  [Thermostat]            climate  22.5°C → 21.0°C   online

[ZONE: Kitchen]
  [Kitchen Ceiling]       light    on    dim:1.0     online
  ...

OFFLINE
  [Garage Sensor]         sensor                     offline 2h

──────────────────────────────────────────────────────
 Actions:
 a) Toggle a specific device
 b) Turn all [filter] off
 c) Filter by zone
 d) View device capabilities
──────────────────────────────────────────────────────
```

For toggle / set capability:

```bash
# Set onoff
homey_call "/devices/device/${DEVICE_ID}/capability/onoff" PUT '{"value": false}'

# Set dim (0.0 - 1.0)
homey_call "/devices/device/${DEVICE_ID}/capability/dim" PUT '{"value": 0.5}'

# Set target temperature
homey_call "/devices/device/${DEVICE_ID}/capability/target_temperature" PUT '{"value": 21.0}'
```

Bulk operations REQUIRE `AskUserQuestion` confirmation (Rule 5 — destructive-like behavior). Show device count + sample before executing.

---

## FLOWS (and SCENES — alias)

Homey calls scenes "flows". Both `flow` and `scene` arguments route here.

```bash
# List flows
homey_call "/flow/flow" | jq '[.[] | {
  id: .id,
  name: .name,
  enabled: .enabled,
  lastExecuted: .lastExecuted,
  triggerable: .triggerable
}]'
```

If `$ARGUMENTS` includes a flow name (e.g. `flow movie-time`, `scene good-night`), fuzzy-match by name (case-insensitive substring), then trigger:

```bash
FLOW_ID="[matched id]"
homey_call "/flow/flow/${FLOW_ID}/trigger" POST '{}'
```

If multiple flows match, present top 4 via `AskUserQuestion` (Rule 1) and let user pick.

If `flow list` or no flow name provided, render the list with last-fired age.

Common scene names users may try: `movie-time`, `good-night`, `leaving-home`, `coming-home`, `wake-up`, `dinner`, `away`. Fuzzy match handles variations (`goodnight`, `good night`, `night`).

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME ► FLOWS — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ENABLED FLOWS ([N])
  [name]                 fired [N min] ago
  [name]                 never
  ...

DISABLED ([N])
  [name]
  ...

──────────────────────────────────────────────────────
 Actions:
 a) Trigger a flow
 b) Enable / disable a flow
 c) View flow definition
──────────────────────────────────────────────────────
```

Trigger results: confirm `{"success": true}` or surface the error.

---

## ENERGY

Live power draw + today's kWh + top consumers + anomaly detection.

```bash
# Live power
homey_call "/energy/live" | jq '{
  totalWatts: .totalPower // 0,
  byDevice: [.devices // {} | to_entries[] | {id: .key, watts: .value.power}] | sort_by(-.watts) | .[:10]
}'

# Today's energy report
TODAY=$(date -u +"%Y-%m-%d")
homey_call "/energy/report?period=today" | jq '{
  todayKwh: .totalEnergy // 0,
  byZone: .zones // {}
}'

# 7-day baseline for anomaly detection
homey_call "/energy/report?period=last7days" | jq '.dailyAverage // 0'
```

Compute anomaly: if `todayKwh > 1.5 * dailyAverage`, flag as anomaly.

Render:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME ► ENERGY — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

LIVE         [W] right now
TODAY        [kWh]    (baseline: [kWh]/day  → [+/-N%])

TOP CONSUMERS (live)
  1. [device]  [W]
  2. [device]  [W]
  ...

ANOMALY      [yes/no]  [reason if yes]

──────────────────────────────────────────────────────
 Actions:
 a) View 30-day energy trend
 b) Estimate monthly cost
 c) Set energy alert threshold
──────────────────────────────────────────────────────
```

If anomaly detected AND cost projection > threshold, route to `/ops:ops-fires` (see Phase 3).

Mobile mode: 3 lines only — `now [W]W · today [kWh] (baseline [kWh]) · top: [device] [W]W`.

---

## CLIMATE

Per-zone temperature, humidity, target temp, heating mode. Allow setting.

```bash
# Climate devices
homey_call "/devices/device" | jq '[.[] | select(
  (.capabilities | index("target_temperature")) or
  (.capabilities | index("measure_temperature")) or
  (.capabilities | index("measure_humidity"))
) | {
  name: .name,
  zone: .zoneName,
  measure_temperature: (.capabilitiesObj.measure_temperature.value // null),
  target_temperature: (.capabilitiesObj.target_temperature.value // null),
  measure_humidity: (.capabilitiesObj.measure_humidity.value // null),
  mode: (.capabilitiesObj.thermostat_mode.value // null)
}] | group_by(.zone)'
```

If `$ARGUMENTS` is `climate <zone> <temp>` (e.g. `climate bedroom 19`), find the thermostat in that zone and set target temp via `AskUserQuestion` confirmation:

```bash
homey_call "/devices/device/${THERMO_ID}/capability/target_temperature" PUT '{"value": 19.0}'
```

Render:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME ► CLIMATE — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[ZONE: Living Room]
  Temperature   21.5°C   target 21.0°C   mode: heat
  Humidity      45%

[ZONE: Bedroom]
  Temperature   19.0°C   target 19.0°C   mode: heat
  Humidity      52%

──────────────────────────────────────────────────────
 Actions:
 a) Set temperature for a zone
 b) Switch mode (heat/cool/off/auto)
 c) View 24h history
──────────────────────────────────────────────────────
```

---

## PRESENCE

Who is home, based on Homey presence detection.

```bash
homey_call "/presence" | jq '[.[] | {
  id: .id,
  name: .name,
  present: .present,
  lastSeen: .lastSeen
}]'
```

Render:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME ► PRESENCE — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HOME ([N])
  [name]    home since [HH:MM]
  ...

AWAY ([N])
  [name]    left [HH:MM]   away [N]h
  ...

──────────────────────────────────────────────────────
 Actions:
 a) Trigger 'leaving-home' flow
 b) Trigger 'coming-home' flow
 c) Check presence-linked automations
──────────────────────────────────────────────────────
```

If house has been empty for > 1 hour, surface "empty since HH:MM" for cross-reference with `/ops:ops-go` briefing.

---

## ALARMS / SECURITY

Active alarms (smoke, water, motion in armed zone, contact). Allow arm/disarm.

```bash
# Active alarms
homey_call "/alarms/alarm" | jq '[.[] | select(.active == true) | {
  type: .type,
  zone: .zoneName,
  device: .deviceName,
  triggeredAt: .triggeredAt,
  severity: .severity
}]'

# Security mode (fw 13.2.0+: /api/manager/system — NOT /system/info, that returns the Web App HTML)
homey_call "/system" | jq '.securityMode // "unknown"'
```

If `$ARGUMENTS` is `alarm arm` or `alarm disarm`, set security mode (use a flow if Homey exposes it as such — typical Homey setups use a "Security Armed" flow):

```bash
# Find the arm/disarm flow by name
FLOW_ID=$(homey_call "/flow/flow" | jq -r '.[] | select(.name | test("arm|security"; "i")) | .id' | head -1)
homey_call "/flow/flow/${FLOW_ID}/trigger" POST '{}'
```

Arm/disarm REQUIRES `AskUserQuestion` confirmation (Rule 5 — security-impacting action).

Render:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 OPS ► HOME ► ALARMS — [timestamp]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

SECURITY MODE     [armed | disarmed | partial]

ACTIVE ALARMS ([N])
  CRITICAL  [smoke]  [zone]  [device]  triggered [time]
  HIGH      [water]  [zone]  [device]  triggered [time]
  MEDIUM    [motion] [zone]  [device]  triggered [time]

NO ACTIVE ALARMS  (if N == 0)

RECENT (last 24h)
  [time]  [type]  [zone]  [device]  — cleared

──────────────────────────────────────────────────────
 Actions:
 a) Arm security
 b) Disarm security
 c) Clear all active alarms
 d) Broadcast critical alarm to /ops:ops-comms
──────────────────────────────────────────────────────
```

**Critical alarm protocol**: If a smoke / water / unauthorized-entry alarm fires, IMMEDIATELY suggest piping a message to `/ops:ops-comms` (WhatsApp + Telegram). Stage the draft and follow Rule 6 (per-message approval). Never auto-send.

Example draft (stage for approval):

```
Channel: WhatsApp + Telegram
To: [user's emergency contacts from preferences.json → emergency_contacts]
Body:
  [HOMEY ALERT] Smoke alarm triggered in [zone] at [time].
  Device: [device name].
  Check the home immediately. Hub: [hub name].
```

Then `AskUserQuestion` with `[Send]` / `[Edit]` / `[Skip]`.

---

## HEALTH (`health`, `diagnose`, `outage`)

Plejd-style mass-outage detector. Groups offline devices by `driverUri` (the source app/integration), then flags any driver where > 30% of its devices are offline as a probable single-driver outage — usually an app crash, expired credentials, or hub-side disconnect inside that integration.

```bash
# Pull devices once, then group/aggregate locally
DEVICES_JSON=$(homey_call "/devices/device")

echo "$DEVICES_JSON" | jq '{
  total: (. | length),
  online: ([.[] | select(.available == true)] | length),
  offline: ([.[] | select(.available == false)] | length),
  drivers: (
    [.[] | {driverUri: (.driverUri // "unknown"), available: .available}]
    | group_by(.driverUri)
    | map({
        driverUri: .[0].driverUri,
        total: length,
        offline: ([.[] | select(.available == false)] | length),
        offline_pct: (([.[] | select(.available == false)] | length) * 100 / length)
      })
    | sort_by(-.offline_pct)
  )
}'
```

Driver short-name: strip `homey:app:` prefix and dotted namespace (e.g. `homey:app:com.plejd` → `com.plejd` → `plejd`). Display logic:

- Total banner: `total devices: N (X online, Y offline)`.
- For each driver where `offline_pct > 30` AND `total
