# Astrion Custom Dashboard — Guida completa a `dashboard.json`
> Lingua: **Italiano** · [English](GUIDE_dashboard_json_EN.md) · [Français](GUIDE_dashboard_json_FR.md) · [Español](GUIA_dashboard_json_ES.md) · [Deutsch](ANLEITUNG_dashboard_json_DE.md)
>
> Riferimento: repository [`dckiller51/astrion-custom-dashboard`](https://github.com/dckiller51/astrion-custom-dashboard), branch `main`, commit `d73ec52` (app 1.1.4). Ogni opzione descritta è stata verificata nel sorgente Kotlin (`config/DashboardLoader.kt`, `config/AppConfig.kt`, `cards/impl/*.kt`). Le versioni successive potrebbero cambiare.
>
> Tutti gli esempi usano **entità fittizie** (`light.soggiorno`, `media_player.salotto`, …): sostituiscile con le tue.
## Indice
1. [Cos'è (e cosa non è)](#1-cosè-e-cosa-non-è)
2. [Dove vive il file e come caricarlo](#2-dove-vive-il-file-e-come-caricarlo)
3. [Struttura radice](#3-struttura-radice)
4. [Pagine](#4-pagine)
5. [Card: regole comuni](#5-card-regole-comuni)
6. [Card senza interfaccia nell'editor](#6-card-senza-interfaccia-nelleditor)
7. [Card con editor](#7-card-con-editor)
8. [Tasti fisici (`hotkeys` / `longHotkeys`)](#8-tasti-fisici-hotkeys--longhotkeys)
9. [Harmony, IR locale e Activity](#9-harmony-ir-locale-e-activity)
10. [Tema](#10-tema)
11. [Esempio completo](#11-esempio-completo)
12. [Risoluzione problemi](#12-risoluzione-problemi)
---
## 1. Cos'è (e cosa non è)
Astrion Custom Dashboard è un'**app Android** che sostituisce il launcher del telecomando **Sanytron Astrion HA100**. Parla con Home Assistant via WebSocket.
- **Non** è una dashboard Lovelace e **non** si installa da HACS.
- Le "card" sono componenti nativi dell'app, non card Lovelace: la sintassi è **JSON**, non YAML.
- Molti nomi di opzione imitano le card Mushroom, ma solo le opzioni elencate qui sono lette dall'app.
## 2. Dove vive il file e come caricarlo
| Elemento | Percorso sul telecomando |
|---|---|
| Configurazione | `/sdcard/astrion/dashboard.json` |
| Icone PNG | `/sdcard/astrion/icons/` |
| Database IR (opzionale) | `/sdcard/astrion/ir-database/` |
Modi per modificarlo:
1. **Editor online** — : form per pagine, card e tasti; scarica il JSON risultante.
2. **Pagina locale** — `http://:8080` (l'indirizzo compare nel pannello Impostazioni): carica/scarica `dashboard.json`, carica icone. Il caricamento ricarica la dashboard **senza riavvio**.
3. **adb** — `adb push dashboard.json /sdcard/astrion/dashboard.json`, poi riapri l'app.
> ⚠️ Se il JSON non è valido l'app **non si blocca**: carica il layout predefinito interno e mostra `dashboard.json invalid (…) — using built-in defaults`. Scarica sempre un backup prima di caricare.
> ⚠️ L'editor online, se riapri una card e la salvi dal form semplice, **riscrive le opzioni da zero**: i campi solo-JSON (sezione 6) vanno persi. Per quelle card usa sempre il campo "Options (raw JSON)".
## 3. Struttura radice
La radice è un oggetto JSON. In alternativa è accettato un **array di card**: diventa un'unica pagina chiamata `Main`.
| Chiave | Tipo | Obbligatoria | Default | Descrizione |
|---|---|---|---|---|
| `pages` | array | **sì** (non vuoto) | — | Pagine, da sinistra a destra |
| `startPage` | intero | no | `0` | Indice (da 0) della pagina all'avvio |
| `hotkeys` | array | no | `[]` | Tasti fisici, pressione breve |
| `longHotkeys` | array | no | `[]` | Tasti fisici, pressione lunga (~500 ms) |
| `irDevices` | array | no | `[]` | Dispositivi IR locali (sezione 9) |
| `activities` | array | no | `[]` | Activity composte (sezione 9) |
| `theme` | oggetto | no | tema scuro | Colori (sezione 10) |
```json
{
"startPage": 0,
"pages": [
{ "name": "Home", "cards": [] }
],
"hotkeys": [],
"longHotkeys": []
}
```
## 4. Pagine
| Chiave | Tipo | Default | Descrizione |
|---|---|---|---|
| `name` | stringa | `"Page"` | Nome; usato per la navigazione (case-insensitive) |
| `cards` | array | `[]` | Card della pagina, dall'alto verso il basso |
| `hotkeys` / `longHotkeys` | array | `[]` | Tasti validi solo su questa pagina; **sovrascrivono** quelli globali con lo stesso `key` |
| `parent` | stringa | — | Pagina "madre": il tasto `parentKey` torna lì |
| `parentKey` | stringa | `"BACK"` | Tasto fisico che torna a `parent` |
| `linkedPage` | stringa | — | Pagina aperta con swipe **verso l'alto** sull'indicatore pagine |
| `hiddenUnlessActivity` | stringa | — | Id di Activity: il pallino della pagina appare solo quando quell'Activity è attiva (la pagina resta raggiungibile) |
| `openWhenEntity` | stringa | — | Entità HA: quando va a `on` la pagina si apre da sola; quando esce da `on` si richiude |
> ℹ️ L'editor online può scrivere anche `openWhenState` / `closeWhenState`, ma nel commit analizzato `DashboardLoader.kt` **non li legge**: lo stato di apertura è sempre `on`. Per un'entità con stati diversi, crea un `binary_sensor` template in HA.
Esempio — pagina citofono che si apre al suono del campanello e si chiude con BACK:
```json
{
"startPage": 0,
"pages": [
{ "name": "Casa", "cards": [] },
{
"name": "Citofono",
"parent": "Casa",
"openWhenEntity": "binary_sensor.campanello",
"cards": [
{ "type": "camera", "options": { "entity_id": "camera.ingresso" } }
]
}
]
}
```
## 5. Card: regole comuni
Ogni card ha questa forma:
```json
{ "type": "light", "options": { "entity_id": "light.soggiorno" } }
```
- `type` è **obbligatorio**; `options` è facoltativo.
- Un `type` non registrato non genera errore di parsing: la card semplicemente non viene disegnata.
- `"pin": "bottom"` dentro `options` (qualsiasi card) la **fissa in fondo** alla pagina, fuori dallo scorrimento.
- **Icone PNG**: percorso assoluto, es. `/sdcard/astrion/icons/netflix.png`.
- **Colori**: `#RRGGBB` o `#AARRGGBB` (alfa per primo). Eccezione: `switch.on_color` accetta solo `#AARRGGBB` (vedi 7.2).
Tipi registrati (21) e supporto nell'editor online:
| Tipo | Editor | Tipo | Editor |
|---|---|---|---|
| `title` | form (+ campi solo JSON) | `media_player` | form |
| `light` | form | `camera` | form |
| `switch` | form | `clock_weather` | form |
| `cover` | form | `vacuum` | form |
| `fan` | form | `plex` | form |
| `climate` | form | `speaker_group` | **solo JSON** |
| `source_select` | form | `monitor` | **solo JSON** |
| `select` | form | `picture_elements` | **solo JSON** |
| `button_grid` | form | `row` | **solo JSON** |
| `scene_grid` | form | `apple_tv_remote` | form |
| `tv_remote` | form | | |
## 6. Card senza interfaccia nell'editor
Nell'editor online scegli il tipo (gruppo *Advanced (raw options)*) e incolla **solo l'oggetto `options`** nel campo *Options (raw JSON)*. Negli esempi sotto è mostrata la card intera: copia ciò che sta dentro `"options"`.
### 6.1 `speaker_group` — gruppo di speaker (Sonos e simili)
Una riga per speaker con: spunta = membro del gruppo, barra volume trascinabile, muto, vol−/vol+. La prima riga è il master.
| Opzione | Tipo | Obbl. | Descrizione |
|---|---|---|---|
| `master` | stringa | **sì** | `media_player` coordinatore del gruppo. Senza, la card non appare |
| `name` | stringa | no | Nome mostrato per il master |
| `speakers` | array | no | Elenco `{ "entity_id", "name" }` degli speaker aggregabili |
Comportamento:
- Spuntare → `media_player.join` sul master con `group_members: [speaker]`, **immediato**.
- Togliere la spunta → `media_player.unjoin` sullo speaker.
- Lo stato "nel gruppo" si legge dall'attributo `group_members` **dello speaker** (deve contenere il master).
- Volume: `media_player.volume_set`, `volume_up`, `volume_down`, `volume_mute`.
```json
{
"type": "speaker_group",
"options": {
"master": "media_player.salotto",
"name": "Salotto",
"speakers": [
{ "entity_id": "media_player.cucina", "name": "Cucina" },
{ "entity_id": "media_player.camera", "name": "Camera" },
{ "entity_id": "media_player.bagno", "name": "Bagno" }
]
}
}
```
### 6.2 `monitor` — elenco valori sensori
Elenco in sola lettura: nome, valore attuale, `unit_of_measurement`. Nessun grafico storico. `unknown` / `unavailable` sono mostrati come `—`.
| Opzione | Tipo | Obbl. | Descrizione |
|---|---|---|---|
| `title` | stringa | no | Intestazione |
| `entities` | array | sì | `{ "entity_id", "name" }`; `name` facoltativo (default: friendly name) |
```json
{
"type": "monitor",
"options": {
"title": "Ambiente",
"entities": [
{ "entity_id": "sensor.temperatura_soggiorno", "name": "Temperatura" },
{ "entity_id": "sensor.umidita_soggiorno", "name": "Umidità" },
{ "entity_id": "sensor.co2_soggiorno" },
{ "entity_id": "sensor.potenza_casa", "name": "Consumo" }
]
}
}
```
### 6.3 `row` — card affiancate
Contenitore orizzontale: le card figlie hanno **larghezza uguale**. Ogni figlia è una card completa (`type` + `options`). È ammessa qualunque card, anche un'altra `row`.
| Opzione | Tipo | Obbl. | Descrizione |
|---|---|---|---|
| `cards` | array | sì | Card figlie |
```json
{
"type": "row",
"options": {
"cards": [
{ "type": "cover", "options": { "entity_id": "cover.tapparella_sx", "name": "Sinistra", "layout": "vertical" } },
{ "type": "cover", "options": { "entity_id": "cover.tapparella_dx", "name": "Destra", "layout": "vertical" } }
]
}
}
```
> 💡 Nelle `row` preferisci `"layout": "vertical"` (light/cover/select): lo spazio orizzontale è ridotto.
### 6.4 `picture_elements` — planimetria interattiva
Immagine di sfondo con icone posizionate in **percentuale**. Può mostrare anche i bersagli di un radar mmWave e il robot aspirapolvere.
| Opzione | Tipo | Default | Descrizione |
|---|---|---|---|
| `image` | stringa | `/sdcard/astrion/floorplan.png` | Percorso PNG/JPG sul telecomando |
| `aspect` | numero | `1.3` | Rapporto L/A usato **finché l'immagine non è caricata** (poi prevale quello reale) |
| `elements` | array | `[]` | Icone (tabella sotto) |
| `radar` | oggetto | — | Overlay radar (6.4.1) |
| `vacuum` | oggetto | — | Overlay aspirapolvere (6.4.2) |
Campi di ogni elemento:
| Campo | Tipo | Default | Descrizione |
|---|---|---|---|
| `left` / `top` | numero | `50` | Centro dell'icona, % di larghezza / altezza |
| `entity_id` | stringa | — | Tocco → **toggle** dell'entità; icona ambra quando è `on` |
| `service` | stringa | — | Usato solo se manca `entity_id`: `dominio.servizio` |
| `targets` | array | — | Entità a cui applicare `service`, una chiamata per entità |
| `icon` | stringa | lampadina | Unico valore alternativo: `"power"` |
```json
{
"type": "picture_elements",
"options": {
"image": "/sdcard/astrion/planimetria.png",
"aspect": 1.5,
"elements": [
{ "entity_id": "light.soggiorno", "left": 25, "top": 35 },
{ "entity_id": "light.cucina", "left": 70, "top": 30 },
{ "entity_id": "switch.lampada_studio", "left": 80, "top": 75 },
{ "service": "light.turn_off", "targets": ["light.soggiorno", "light.cucina"], "icon": "power", "left": 95, "top": 6 }
]
}
}
```
> 💡 Per trovare le coordinate: apri l'immagine in un editor grafico, prendi X/Y in pixel e calcola `left = X / larghezza × 100`, `top = Y / altezza × 100`.
#### 6.4.1 Overlay `radar` (mmWave, es. LD2450)
Disegna fino a `targets` pallini numerati. Legge le entità `__x` e `__y` con n = 1…targets. Se l'entità manca o non è numerica, il pallino non appare.
| Campo | Default | Descrizione |
|---|---|---|
| `prefix` | — (**obbl.**) | Prefisso sensori, es. `sensor.radar_soggiorno_target` |
| `targets` | `3` | Numero bersagli |
| `origin_left` / `origin_top` | `50` / `10` | Posizione del sensore sull'immagine (%) |
| `scale_x` / `scale_y` | `8` / `8` | % di immagine per **unità** del valore del sensore |
| `scale_x_right` | = `scale_x` | Scala X per i valori ≥ 0 (lato destro) |
| `top_offset_left` | `0` | Spostamento verticale extra (%) per il lato sinistro |
| `rotation` | `0` | Rotazione in gradi |
| `flip_x` / `flip_y` | `false` | Specchia gli assi |
| `blend` | `"overlay"` | `none`, `multiply`, `screen`, `softlight`, `hardlight`, `difference`, `overlay` |
Formula applicata (per capire la taratura): `left = origin_left + x_ruotato × scala_x`, `top = origin_top + y_ruotato × scale_y`. Risultato limitato a 0–100 %.
```json
{
"type": "picture_elements",
"options": {
"image": "/sdcard/astrion/planimetria.png",
"elements": [],
"radar": {
"prefix": "sensor.radar_soggiorno_target",
"targets": 3,
"origin_left": 50,
"origin_top": 5,
"scale_x": 10,
"scale_y": 10,
"rotation": 0,
"flip_x": false,
"flip_y": false,
"blend": "overlay"
}
}
}
```
> 💡 Taratura: mettiti in un punto noto, guarda dove appare il pallino, correggi prima `origin_*`, poi `scale_*`, infine `rotation`/`flip_*`. L'unità di `scale_*` dipende da come il tuo firmware espone x/y (mm, cm o m).
#### 6.4.2 Overlay `vacuum`
Icona del robot posizionata **per stanza** (HA non espone X/Y del robot, solo la stanza corrente). Tocco → popup con tutti i comandi della card `vacuum` (7.12). Accetta le stesse opzioni di `vacuum` più:
| Campo | Descrizione |
|---|---|
| `entity_id` | Entità `vacuum.*` (**obbl.**) |
| `room_entity` | Sensore con il nome della stanza corrente |
| `room_positions` | Mappa `"NomeStanza": [left, top]` in % |
| `dock_position` | `[left, top]` della base |
```json
{
"type": "picture_elements",
"options": {
"image": "/sdcard/astrion/planimetria.png",
"elements": [],
"vacuum": {
"entity_id": "vacuum.robot",
"name": "Robot",
"map_image": "image.robot_mappa",
"rooms": [
{ "name": "Cucina", "id": 16 },
{ "name": "Soggiorno", "id": 17 }
],
"room_entity": "sensor.robot_stanza_corrente",
"room_positions": { "Cucina": [70, 30], "Soggiorno": [25, 40] },
"dock_position": [5, 60]
}
}
}
```
### 6.5 Campi solo-JSON della card `title`
La card `title` ha un form, ma titolo e sottotitolo **cliccabili** si configurano solo nel JSON. Si usa lo stesso vocabolario di azioni di `scene_grid` (7.10) con prefisso `title_` o `subtitle_`:
| Campo | Azione |
|---|---|
| `title_entity_id` | Attiva scena/script |
| `title_page` | Apre la pagina |
| `title_activityId` (+ `title_hub`) | Avvia Activity Harmony |
| `title_harmonyDevice` + `title_harmonyCommand` (+ `title_hub`) | Comando IR via Harmony |
| `title_irDevice` + `title_irCommand` | Comando IR locale |
| `title_activity` | Avvia Activity composta |
Esempio — sottotitolo "Vedi tutte →" che apre la pagina Luci:
```json
{
"type": "title",
"options": {
"title": "Soggiorno",
"subtitle": "Vedi tutte le luci →",
"subtitle_page": "Luci",
"icon": "/sdcard/astrion/icons/sofa.png",
"divider": true,
"color": "#7FB3C4"
}
}
```
## 7. Card con editor
Anche queste si possono scrivere a mano. Sono elencate tutte le opzioni lette dal codice.
### 7.1 `title`
| Opzione | Default | Descrizione |
|---|---|---|
| `title` / `subtitle` | — | Testi |
| `alignment` | `"start"` | `start`, `center`, `end`, `justify` |
| `icon` | — | PNG prima del titolo (forza allineamento a sinistra della riga titolo) |
| `divider` | `false` | Linea che riempie la riga dopo il titolo |
| `color` | tema | Colore titolo e linea |
### 7.2 `switch`
| Opzione | Descrizione |
|---|---|
| `entity_id` | Entità commutabile (`switch.*`, `input_boolean.*`, …) |
| `name` | Etichetta |
| `icon` | `heater`/`heat`, `fan`, `bulb`/`light`; altro → accensione |
| `on_color` | Colore quando acceso. **Solo `#AARRGGBB`** (un valore `#RRGGBB` risulta trasparente) |
```json
{ "type": "switch", "options": { "entity_id": "switch.stufa", "name": "Stufa", "icon": "heater", "on_color": "#B3902828" } }
```
### 7.3 `light`
Tocco = toggle; pressione lunga = popup dettagli.
| Opzione | Default | Descrizione |
|---|---|---|
| `entity_id`, `name` | — | |
| `layout` | `default` | `default`, `horizontal`, `vertical` |
| `use_light_color` | `false` | Icona tinta col colore RGB della luce |
| `show_brightness` | `true` | Stato "N%" invece di "On" |
| `show_brightness_control` | vedi nota | Slider luminosità |
| `show_color_temp_control` | `false` | Slider temperatura colore |
| `show_color_control` | `false` | Campioni di colore |
| `collapsible_controls` | `false` | Nasconde i controlli a luce spenta |
> Se **nessuno** dei tre `show_*_control` è presente, appare solo lo slider luminosità. Appena ne imposti uno, appaiono solo quelli a `true`. Con più controlli, un pulsante a freccia li alterna.
```json
{ "type": "light", "options": { "entity_id": "light.soggiorno", "name": "Soggiorno", "layout": "horizontal", "use_light_color": true, "show_brightness_control": true, "show_color_control": true } }
```
### 7.4 `cover`
Pressione lunga = popup con preset 25/50/75 %.
| Opzione | Default | Descrizione |
|---|---|---|
| `entity_id`, `name`, `layout` | | come `light` |
| `show_buttons_control` | vedi nota | Apri/stop/chiudi |
| `show_position_control` | `false` | Slider posizione |
| `show_tilt_position_control` | `false` | Slider inclinazione |
> Stessa logica di `light`: senza nessun flag appaiono i pulsanti. Slider mostrati solo se l'entità espone l'attributo relativo.
### 7.5 `fan`
| Opzione | Default | Descrizione |
|---|---|---|
| `entity_id`, `name` | | |
| `style` | `auto` | `auto`, `simple`, `step`, `full` |
| `preset_modes` | dall'entità | Array, ordine e filtro dei preset |
| `step` | `percentage_step` dell'entità, poi `20` | Passo % |
| `show_captions` | `true` | Didascalie sopra le righe di chip |
### 7.6 `climate`
| Opzione | Default | Descrizione |
|---|---|---|
| `entity_id`, `name` | | |
| `step` | `1.0` | Passo setpoint. **Ignorato** se l'entità espone `target_temp_step` |
| `hvac_modes` / `fan_modes` / `swing_modes` | dall'entità | Array: ordine e filtro dei chip (`off` è sempre escluso) |
| `hvac_mode_style` | `icons` | `icons` o `label` |
| `fan_mode_style` / `swing_mode_style` | `label` | `icons` o `label` |
| `show_captions` | `true` | Didascalie |
```json
{ "type": "climate", "options": { "entity_id": "climate.soggiorno", "name": "Clima", "step": 0.5, "hvac_modes": ["heat", "cool", "auto"], "fan_mode_style": "icons" } }
```
### 7.7 `select` e `source_select`
- `select`: per `input_select.*` / `select.*`. Opzioni: `entity_id`, `name`, `icon_color`, `layout`.
- `source_select`: sorgente di un `media_player` (`source_list`). Opzioni: `entity_id`, `name`.
### 7.8 `media_player`
Compatto: tocco = play/pausa; pressione lunga = dettagli.
| Opzione | Default | Descrizione |
|---|---|---|
| `entity_id`, `name` | | |
| `variant` | compatto | `"full"` per la versione grande |
| `use_media_info` | `true` | Titolo/artista invece del nome |
| `show_volume_level` | `false` | Aggiunge "⸱ N%" |
| `media_controls` | `previous,play_pause,next` | Lista con virgole: `on_off`, `shuffle`, `previous`, `play_pause`, `next`, `repeat` |
| `volume_controls` | `mute,buttons` | `mute`, `buttons`, `set` (slider) |
| `top_buttons` | — | Solo `full`: `{ "name", "service", "entity_id", "data" }` |
I pulsanti non supportati dall'entità (`supported_features`) vengono nascosti.
### 7.9 `button_grid`
| Opzione | Default | Descrizione |
|---|---|---|
| `columns` | `3` | Colonne |
| `buttons` | — | `{ "name", "icon", "service", "entity_id", "data" }` |
```json
{
"type": "button_grid",
"options": {
"columns": 2,
"buttons": [
{ "name": "Netflix", "icon": "/sdcard/astrion/icons/netflix.png", "service": "media_player.play_media", "entity_id": "media_player.tv", "data": { "media_content_type": "app", "media_content_id": "com.netflix.ninja" } },
{ "name": "Buonanotte", "service": "script.buonanotte" }
]
}
}
```
### 7.10 `scene_grid`
| Opzione | Default | Descrizione |
|---|---|---|
| `columns` | `2` | Colonne |
| `layout` | griglia | `"row"` = riga scorrevole |
| `show_labels` | `true` | Nome sotto l'icona |
| `icon_fill` | `false` | Icona a tutta piastrella |
| `tile_height` | `74` (`120` con `icon_fill`) | Altezza piastrella (dp) |
| `scenes` | — | Piastrelle (sotto) |
Campi di ogni piastrella: `name`, `icon`, `color`, e **un'azione**: `entity_id` (scena/script), `page`, `activityId` (+`hub`), `harmonyDevice`+`harmonyCommand` (+`hub`), `irDevice`+`irCommand`, `activity`. Opzionali `track` + `room` (+`devices`) per tracciarla come Activity. Con `activity` il campo `page` viene ignorato (la pagina si imposta sull'Activity).
### 7.11 `camera`
| Opzione | Default | Descrizione |
|---|---|---|
| `entity_id` | — (**obbl.**) | `camera.*` |
| `name` | friendly name | |
| `mode` | `stream` | `stream` (MJPEG) o `snapshot` |
| `snapshot_interval` | `2` | Secondi tra fotogrammi |
| `aspect` | `1.777` | Rapporto L/A |
| `fit` | `cover` | `cover` o `contain` |
Tocco = schermo intero con zoom (pinch 1×–8×).
### 7.12 `vacuum`
| Opzione | Descrizione |
|---|---|
| `entity_id`, `name` | |
| `map_image` | Entità `image.*` della mappa |
| `map_rotation` | Rotazione mappa (gradi) |
| `map_height` | Altezza mappa (dp) |
| `rooms` | `{ "name", "id" }` — id segmento della mappa |
| `room_clean_action` | Default Roborock/Xiaomi (`vacuum.send_command app_segment_clean`); altrimenti `{ "domain", "service", "parameter" }` |
### 7.13 `clock_weather`
Opzioni: `entity_id` (`weather.*`), `time_format` (`12` default o `24`), `forecast_rows` (default `4`), `calendar_entity`.
### 7.14 `tv_remote`
| Opzione | Descrizione |
|---|---|
| `name` | Default `TV` |
| `remote_entity` | `remote.*` (**obbl.**) — invia `remote.send_command` |
| `mute_entity` | Entità alternativa per il muto |
| `media_entity` | `media_player` usato per lanciare le app |
| `commands` | Rimappa i comandi: `up`, `down`, `left`, `right`, `center`, `back`, `home`, `menu`, `power` |
| `apps` | `{ "name", "app" }` oppure `{ "name", "service", "entity_id", "data" }` |
### 7.15 `apple_tv_remote`
Comandi inviati **direttamente all'hub Harmony**. Opzioni: `deviceId` (id dispositivo Harmony), `hub` (facoltativo).
### 7.16 `plex`
Opzioni: `host`, `token`, `media_entity`, `play_entity`, `play_content_type` (`video`/`url`), `source` (default `Plex`), `show_on_deck`, `show_recently_added_movies`, `show_recently_added_shows` (default `true`), `items_per_row` (default `12`).
> ⚠️ Il token Plex è in chiaro nel file: non condividere `dashboard.json`.
## 8. Tasti fisici (`hotkeys` / `longHotkeys`)
Nomi `key` disponibili sull'HA100:
`UP DOWN LEFT RIGHT CENTER`, `PAGE_UP PAGE_DOWN`, `VOLUME_UP VOLUME_DOWN MUTE`, `BACK HOME POWER VOICE`, `LIGHT CURTAIN SCENE AC`, `CUSTOM_1…CUSTOM_4`.
Campi (nota: qui l'entità si chiama **`entityId`**, in camelCase):
| Campo | Descrizione |
|---|---|
| `key` | Tasto (**obbl.**) |
| `openOverlay` | `settings` o `activities` |
| `openCurrentActivityRoom` | Nome stanza: apre la pagina dell'Activity attiva lì |
| `page` | Apre la pagina |
| `harmonyDevice` + `harmonyCommand` / `harmonyActivity` (+ `hub`) | Harmony (`harmonyActivity: "-1"` = spegni) |
| `irDevice` + `irCommand` | IR locale |
| `service` + `entityId` + `data` | Servizio HA |
| `track` + `room` (+ `devices`) | Traccia come Activity |
Usa **un'azione per tasto**. Priorità: `openOverlay` → `openCurrentActivityRoom` → `page` → … → `service`.
```json
{ "key": "CUSTOM_1", "service": "media_player.play_media", "entityId": "media_player.tv", "data": { "media_content_type": "app", "media_content_id": "com.netflix.ninja" } }
```
> 💡 Frecce e volume si ripetono tenendo premuto: non assegnare loro `longHotkeys`.
## 9. Harmony, IR locale e Activity
**`irDevices`** — dispositivi IR pilotati dal trasmettitore del telecomando:
```json
{
"startPage": 0,
"pages": [ { "name": "TV", "cards": [] } ],
"irDevices": [
{
"id": "tv_salotto",
"name": "TV Salotto",
"commands": {
"power": { "freq": 38000, "pattern": [9000, 4500, 560, 560] }
}
}
]
}
```
In alternativa a `commands`: `category` + `brand` + `model` (database in `/sdcard/astrion/ir-database/`). Campo `target`: `"local"` (default) o `{ "extender": "" }`.
**`activities`** — scenari multi-dispositivo:
| Campo | Descrizione |
|---|---|
| `id`, `name`, `room` | `id` e `room` **obbligatori** |
| `icon`, `page` | Icona; pagina aperta all'avvio |
| `devices` | **Obbligatorio, non vuoto**: `deviceId`, `source` (`ir`/`harmony`/`ha`), `hub`, `powerOnCommand`, `powerOffCommand`, `inputCommand`, `powerOnFirst` (default `true`), `powerOffOnExit` (default `true`), `delayAfterMs` |
| `volumeDeviceId`, `volumeUpCommand`, `volumeDownCommand`, `muteCommand` | Controllo volume |
La configurazione Harmony (IP/ID hub) si fa dalla pagina `:8080`, non nel JSON.
## 10. Tema
Tutte facoltative (default fra parentesi): `background` (#0E2229), `cardSurface` (#1B343D), `insetSurface` (#152B33), `controlBackground` (#2C4C58), `primaryText` (#E6F0F1), `mutedText` (#93AFB6), `iconTint` (#CBDCE0), `accent` (#6EA8FE), `accentSecondary` (#4C6EF5), `amber` (#FFC24B), `danger` (#E06767), `success` (#4CAF50).
## 11. Esempio completo
Cinque pagine: Luci, Casa (planimetria + sensori), Musica (gruppo speaker), Clima, Citofono (auto-apertura).
```json
{
"startPage": 1,
"theme": { "background": "#0E2229", "accent": "#6EA8FE" },
"pages": [
{
"name": "Luci",
"cards": [
{ "type": "title", "options": { "title": "Luci", "subtitle": "Soggiorno e cucina", "divider": true, "subtitle_page": "Casa" } },
{ "type": "light", "options": { "entity_id": "light.soggiorno", "name": "Soggiorno", "show_brightness_control": true, "show_color_temp_control": true } },
{ "type": "row", "options": { "cards": [
{ "type": "switch", "options": { "entity_id": "switch.stufa", "name": "Stufa", "icon": "heater", "on_color": "#B3902828" } },
{ "type": "switch", "options": { "entity_id": "switch.ventilatore", "name": "Ventilatore", "icon": "fan" } }
] } },
{ "type": "scene_grid", "options": { "layout": "row", "pin": "bottom", "scenes": [
{ "entity_id": "scene.cena", "name": "Cena", "color": "#66FFB300" },
{ "entity_id": "script.tutto_spento", "name": "Spegni", "color": "#66000000" }
] } }
]
},
{
"name": "Casa",
"cards": [
{ "type": "clock_weather", "options": { "entity_id": "weather.casa", "time_format": 24, "forecast_rows": 3 } },
{ "type": "picture_elements", "options": {
"image": "/sdcard/astrion/planimetria.png",
"aspect": 1.3,
"elements": [
{ "entity_id": "light.soggiorno", "left": 30, "top": 40 },
{ "entity_id": "light.cucina", "left": 70, "top": 55 },
{ "service": "light.turn_off", "targets": ["light.soggiorno", "light.cucina"], "icon": "power", "left": 92, "top": 8 }
]
} },
{ "type": "monitor", "options": { "title": "Sensori", "entities": [
{ "entity_id": "sensor.temperatura_soggiorno", "name": "Soggiorno" },
{ "entity_id": "sensor.umidita_soggiorno", "name": "Umidità" },
{ "entity_id": "sensor.potenza_casa", "name": "Consumo" }
] } }
]
},
{
"name": "Musica",
"cards": [
{ "type": "media_player", "options": { "entity_id": "media_player.salotto", "variant": "full", "media_controls": "shuffle,previous,play_pause,next,repeat", "volume_controls": "mute,set" } },
{ "type": "speaker_group", "options": { "master": "media_player.salotto", "name": "Salotto", "speakers": [
{ "entity_id": "media_player.cucina", "name": "Cucina" },
{ "entity_id": "media_player.camera", "name": "Camera" }
] } },
{ "type": "source_select", "options": { "entity_id": "media_player.salotto", "name": "Sorgente" } }
]
},
{
"name": "Clima",
"cards": [
{ "type": "climate", "options": { "entity_id": "climate.soggiorno", "name": "Clima", "step": 0.5 } },
{ "type": "cover", "options": { "entity_id": "cover.tapparella_soggiorno", "name": "Tapparella", "show_buttons_control": true, "show_position_control": true } }
]
},
{
"name": "Citofono",
"parent": "Casa",
"openWhenEntity": "binary_sensor.campanello",
"cards": [
{ "type": "camera", "options": { "entity_id": "camera.ingresso", "mode": "stream", "fit": "cover" } }
]
}
],
"hotkeys": [
{ "key": "LIGHT", "page": "Luci" },
{ "key": "CURTAIN", "page": "Casa" },
{ "key": "SCENE", "page": "Musica" },
{ "key": "AC", "page": "Clima" },
{ "key": "VOLUME_UP", "service": "media_player.volume_up", "entityId": "media_player.salotto" },
{ "key": "VOLUME_DOWN", "service": "media_player.volume_down", "entityId": "media_player.salotto" },
{ "key": "VOICE", "openOverlay": "settings" }
],
"longHotkeys": [
{ "key": "LIGHT", "service": "script.turn_on", "entityId": "script.tutto_spento" }
]
}
```
## 12. Risoluzione problemi
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Appare la dashboard di esempio | JSON non valido o `pages` mancante/vuoto | Valida il JSON (es. `python -m json.tool dashboard.json`) |
| Una card non appare | `type` errato, oppure manca l'opzione obbligatoria (`master`, `remote_entity`, `entity_id`) | Controlla nome tipo e opzioni |
| Campi persi dopo modifica nell'editor | Salvataggio dal form semplice | Usa "Options (raw JSON)" |
| `switch` acceso ma colore invisibile | `on_color` a 6 cifre | Usa `#AARRGGBB` |
| Icone PNG assenti | Percorso relativo o file non caricato | Percorso assoluto `/sdcard/astrion/icons/…` |
| Hotkey servizio senza effetto | `entity_id` invece di `entityId` | Nei tasti usa `entityId` |
| Pallini radar assenti | Entità `__x/_y` inesistenti o non numeriche | Verifica i nomi in Strumenti per sviluppatori → Stati |
| Pagina auto-apertura non si apre | L'entità non va a `on` | Usa un'entità binaria o un `binary_sensor` template |
| Speaker sempre "non nel gruppo" | L'integrazione non espone `group_members` sullo speaker | Verifica l'attributo in HA |