feat: expose scene presets as voice-controllable scene.* entities (0.5.0)

Add a SCENE platform (scene.py) so each scene preset becomes a scene.*
entity. Alexa / Google Assistant / HomeKit expose the scene, light, and
switch domains — but NOT button — so the existing button.* scene entities
are invisible to voice assistants. Scenes use a distinct unique_id so both
coexist (buttons for dashboards, scenes for voice).

- Register Platform.SCENE; bump manifest 0.4.0 -> 0.5.0
- README: "Voice Control (Alexa / Google / Siri)" guide (Nabu Casa cloud,
  manual skill linking, HomeKit bridge, voice-friendly naming)
This commit is contained in:
2026-06-23 00:50:38 +03:00
parent eca6cb516a
commit f101ca4c73
4 changed files with 118 additions and 2 deletions
+35 -1
View File
@@ -2,7 +2,7 @@
Custom Home Assistant integration for [LedGrab](https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab) — ambient lighting system that captures screen content and drives LED strips in real time.
Creates light, switch, sensor, number, and select entities for each configured device, allowing full control and automation from Home Assistant.
Creates light, switch, scene, sensor, number, and select entities for each configured device, allowing full control and automation from Home Assistant — including hands-free voice control via Alexa, Google Assistant, and Siri.
## Installation
@@ -47,6 +47,40 @@ automation:
entity_id: switch.living_room_tv_processing
```
## Voice Control (Alexa / Google / Siri)
Scene presets are exposed both as **`scene.*`** entities (new in 0.5.0) and as `button.*` entities. **For voice control, use the `scene.*` entities** — Alexa, Google Assistant, and HomeKit/Siri expose `scene`, `light`, and `switch` domains, but **not** `button`. Capture targets are `switch.*_processing` entities and `api_input` color sources are `light.*` entities, both of which are also voice-controllable.
### Prerequisites
Install the LedGrab integration (above) and create the entities you want to control. Confirm you have:
- `scene.*` entities for your scene presets (e.g. `scene.movie_night`)
- `switch.*_processing` for your capture targets
- `light.*` for any `api_input` color sources
### Alexa & Google Assistant — easy path (Nabu Casa)
1. Subscribe to **Home Assistant Cloud** (Nabu Casa).
2. **Settings → Home Assistant Cloud → Alexa / Google Assistant** → enable.
3. **Settings → Voice assistants → Expose** → tick the LedGrab `scene`, `switch`, and `light` entities.
4. Discover devices: *"Alexa, discover devices"* (or open the Google Home app).
5. Say *"Alexa, turn on Movie Night"* or *"Hey Google, turn off the TV ambient light."*
### Alexa & Google Assistant — manual path (no subscription)
Link the official **Home Assistant** Alexa skill / Google Assistant action against an externally reachable HA URL with a long-lived access token, then expose the same domains. See HA's [`cloud`](https://www.home-assistant.io/integrations/cloud/), [`google_assistant`](https://www.home-assistant.io/integrations/google_assistant/), and [`alexa`](https://www.home-assistant.io/integrations/alexa/) component docs.
### Siri — HomeKit Bridge
Add the **HomeKit Bridge** integration (`homekit:`), include the LedGrab `scene` / `light` / `switch` entities in its `filter`, then scan the pairing QR code in the iOS Home app. Say *"Hey Siri, turn on Movie Night."*
### Voice-friendly naming tips
- Rename entities in HA so the friendly name is what you'll say (e.g. `scene.movie_night` → "Movie Night").
- Add **Aliases** in the Expose dialog so multiple phrasings work.
- Avoid the generic auto-names ("Light", "Processing").
## Requirements
- Home Assistant 2023.1.0 or later
+1
View File
@@ -31,6 +31,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.LIGHT,
Platform.SCENE,
Platform.SWITCH,
Platform.SENSOR,
Platform.NUMBER,
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"issue_tracker": "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration/issues",
"requirements": ["aiohttp>=3.9.0"],
"version": "0.4.0"
"version": "0.5.0"
}
+81
View File
@@ -0,0 +1,81 @@
"""Scene platform for LedGrab — scene presets as voice-activatable scenes.
Scene presets are also exposed as ``button.*`` entities (see button.py) for
dashboard use. Buttons, however, are NOT in the set of entity domains that
Alexa / Google Assistant / HomeKit expose to voice assistants — ``scene.*`` is.
This platform adds a ``scene.*`` entity per preset so "Alexa, turn on Movie
Night" works, while leaving the buttons in place for existing dashboards.
"""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import LedGrabCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a scene entity per scene preset."""
data = hass.data[DOMAIN][entry.entry_id]
coordinator: LedGrabCoordinator = data[DATA_COORDINATOR]
entities: list[Scene] = []
if coordinator.data:
for preset in coordinator.data.get("scene_presets", []):
entities.append(LedGrabScene(coordinator, preset, entry.entry_id))
async_add_entities(entities)
class LedGrabScene(CoordinatorEntity, Scene):
"""A scene preset exposed as a HA scene (voice-friendly)."""
# Full standalone name so utterances like "turn on Movie Night" match,
# instead of the "<device> <entity>" composite that has_entity_name gives.
_attr_has_entity_name = False
_attr_icon = "mdi:palette"
def __init__(
self,
coordinator: LedGrabCoordinator,
preset: dict[str, Any],
entry_id: str,
) -> None:
"""Initialize the scene."""
super().__init__(coordinator)
self._preset_id = preset["id"]
self._entry_id = entry_id
# Distinct from the button's unique_id so both can coexist.
self._attr_unique_id = f"{entry_id}_scene_entity_{preset['id']}"
self._attr_name = preset["name"]
@property
def device_info(self) -> dict[str, Any]:
"""All scene entities belong to the same Scenes device as the buttons."""
return {"identifiers": {(DOMAIN, f"{self._entry_id}_scenes")}}
@property
def available(self) -> bool:
"""Return if the underlying preset still exists."""
if not self.coordinator.data:
return False
return self._preset_id in {
p["id"] for p in self.coordinator.data.get("scene_presets", [])
}
async def async_activate(self, **kwargs: Any) -> None:
"""Activate the scene preset."""
await self.coordinator.activate_scene(self._preset_id)