Skip to content

Commit 3559815

Browse files
author
Tom Lasswell
committed
feat: Add HDMI source selection for AI Sync Box (H6604)
Adds support for devices with devices.capabilities.mode/hdmiSource capability: - New select entity for HDMI input selection (1-4) - ModeCommand class for mode-type capabilities - State tracking for hdmi_source - Updated README with debug logging instructions Closes #3
1 parent b886782 commit 3559815

13 files changed

Lines changed: 442 additions & 4 deletions

File tree

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Enter your API key. Want instant updates? Add your Govee email/password for MQTT
6060
| **LED Lights & Strips** | On/off, brightness, RGB, color temp |
6161
| **RGBIC Strips** | All the above + per-segment colors |
6262
| **Fans** | On/off, speed (Low/Medium/High), oscillation, preset modes |
63+
| **HDMI Sync Boxes** | On/off, HDMI input selection (1-4) |
6364

6465
> **Note:** Cloud-enabled devices only. Bluetooth-only devices need a different integration.
6566
@@ -81,7 +82,65 @@ No credentials? Polling works fine (every 60 seconds by default).
8182
| Slow updates | Enable MQTT or reduce poll interval in options |
8283
| Rate limit errors | Increase poll interval (Govee allows 100 req/min) |
8384

84-
Need debug logs? Add `custom_components.govee: debug` to your logger config.
85+
---
86+
87+
## Debug Logging
88+
89+
When troubleshooting issues, enable debug logging to capture detailed information about what the integration is doing.
90+
91+
### Enable Debug Logging
92+
93+
**Option 1: From the Integration (Recommended)**
94+
1. Go to **Settings****Devices & Services**
95+
2. Find the **Govee** integration card
96+
3. Click the **three dots menu** (⋮) on the integration card
97+
4. Select **Enable debug logging**
98+
5. Reproduce the issue (turn on a light, trigger the error, etc.)
99+
6. Return to the integration card, click the three dots menu again
100+
7. Select **Disable debug logging**
101+
8. Your browser will download a log file automatically
102+
103+
**Option 2: Via configuration.yaml**
104+
105+
For issues during startup or if you need persistent debug logging, add to your `configuration.yaml` and restart:
106+
107+
```yaml
108+
logger:
109+
default: info
110+
logs:
111+
custom_components.govee: debug
112+
```
113+
114+
### Viewing Logs
115+
116+
- Go to **Settings** → **System** → **Logs**
117+
- Click **Load Full Logs** to see everything
118+
- Use the search box to filter for "govee"
119+
120+
See [Home Assistant Logger docs](https://www.home-assistant.io/integrations/logger/) for more details.
121+
122+
### Gathering Logs for Issues
123+
124+
When opening an issue, include relevant log entries. Here's what to capture:
125+
126+
1. **Enable debug logging** (see above)
127+
2. **Reproduce the issue** (turn on/off a device, change a scene, etc.)
128+
3. **Copy the relevant log entries**
129+
130+
**What to include:**
131+
- Logs from when Home Assistant starts (shows device discovery)
132+
- Logs from when the issue occurs
133+
- Any error messages or tracebacks
134+
135+
**Example log snippet to include:**
136+
```
137+
2024-01-15 10:30:45 DEBUG (MainThread) [custom_components.govee.coordinator] Device: Living Room Light (AA:BB:CC:DD:EE:FF:00:11) type=devices.types.light
138+
2024-01-15 10:30:45 DEBUG (MainThread) [custom_components.govee.coordinator] Capability: type=devices.capabilities.on_off instance=powerSwitch
139+
```
140+
141+
**Before posting**, redact sensitive information:
142+
- Replace device IDs with `XX:XX:XX:XX:XX:XX:XX:XX`
143+
- Remove any email addresses or account IDs
85144

86145
---
87146

custom_components/govee/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ async def _async_cleanup_orphaned_entities(
244244
"_scene_select",
245245
"_diy_scene_select",
246246
"_diy_style_select",
247+
"_hdmi_source_select",
247248
"_refresh_scenes",
248249
"_night_light",
249250
"_music_mode",

custom_components/govee/coordinator.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,9 +667,11 @@ def _apply_optimistic_update(
667667
BrightnessCommand,
668668
ColorCommand,
669669
ColorTempCommand,
670+
ModeCommand,
670671
PowerCommand,
671672
SceneCommand,
672673
)
674+
from .models.device import INSTANCE_HDMI_SOURCE
673675

674676
if isinstance(command, PowerCommand):
675677
state.apply_optimistic_power(command.power_on)
@@ -681,6 +683,9 @@ def _apply_optimistic_update(
681683
state.apply_optimistic_color_temp(command.kelvin)
682684
elif isinstance(command, SceneCommand):
683685
state.apply_optimistic_scene(str(command.scene_id))
686+
elif isinstance(command, ModeCommand):
687+
if command.mode_instance == INSTANCE_HDMI_SOURCE:
688+
state.apply_optimistic_hdmi_source(command.value)
684689

685690
async def async_get_scenes(
686691
self,

custom_components/govee/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
"integration_type": "hub",
1010
"iot_class": "cloud_push",
1111
"issue_tracker": "https://github.com/lasswellt/govee-homeassistant/issues",
12-
"loggers": ["aiohttp", "aiomqtt"],
12+
"loggers": ["custom_components.govee", "aiohttp", "aiomqtt"],
1313
"requirements": [
1414
"aiohttp-retry>=2.8.3",
1515
"aiomqtt>=2.0.0",
1616
"cryptography>=41.0.0"
1717
],
1818
"ssdp": [],
19-
"version": "2026.1.51",
19+
"version": "2026.1.52",
2020
"zeroconf": []
2121
}

custom_components/govee/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ColorTempCommand,
1010
DeviceCommand,
1111
DIYSceneCommand,
12+
ModeCommand,
1213
OscillationCommand,
1314
PowerCommand,
1415
SceneCommand,
@@ -47,5 +48,6 @@
4748
"ToggleCommand",
4849
"OscillationCommand",
4950
"WorkModeCommand",
51+
"ModeCommand",
5052
"create_night_light_command",
5153
]

custom_components/govee/models/commands.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .device import (
1414
CAPABILITY_COLOR_SETTING,
1515
CAPABILITY_DYNAMIC_SCENE,
16+
CAPABILITY_MODE,
1617
CAPABILITY_ON_OFF,
1718
CAPABILITY_RANGE,
1819
CAPABILITY_SEGMENT_COLOR,
@@ -276,3 +277,27 @@ def instance(self) -> str:
276277

277278
def get_value(self) -> dict[str, int]:
278279
return {"workMode": self.work_mode, "modeValue": self.mode_value}
280+
281+
282+
@dataclass(frozen=True)
283+
class ModeCommand(DeviceCommand):
284+
"""Command to set a mode value (e.g., HDMI source).
285+
286+
This is a generic command for mode-type capabilities that take
287+
an integer value. Used for HDMI source selection on devices
288+
like the Govee AI Sync Box (H6604).
289+
"""
290+
291+
mode_instance: str # e.g., "hdmiSource"
292+
value: int # e.g., 1, 2, 3, 4 for HDMI ports
293+
294+
@property
295+
def capability_type(self) -> str:
296+
return CAPABILITY_MODE
297+
298+
@property
299+
def instance(self) -> str:
300+
return self.mode_instance
301+
302+
def get_value(self) -> int:
303+
return self.value

custom_components/govee/models/device.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
CAPABILITY_TOGGLE = "devices.capabilities.toggle"
2222
CAPABILITY_WORK_MODE = "devices.capabilities.work_mode"
2323
CAPABILITY_PROPERTY = "devices.capabilities.property"
24+
CAPABILITY_MODE = "devices.capabilities.mode"
2425

2526
# Device type constants
2627
DEVICE_TYPE_LIGHT = "devices.types.light"
@@ -42,6 +43,7 @@
4243
INSTANCE_TIMER = "timer"
4344
INSTANCE_OSCILLATION = "oscillationToggle"
4445
INSTANCE_WORK_MODE = "workMode"
46+
INSTANCE_HDMI_SOURCE = "hdmiSource"
4547

4648

4749
@dataclass(frozen=True)
@@ -195,6 +197,11 @@ def is_work_mode(self) -> bool:
195197
"""Check if this is a work mode capability (for fans)."""
196198
return self.type == CAPABILITY_WORK_MODE and self.instance == INSTANCE_WORK_MODE
197199

200+
@property
201+
def is_hdmi_source(self) -> bool:
202+
"""Check if this is an HDMI source mode capability."""
203+
return self.type == CAPABILITY_MODE and self.instance == INSTANCE_HDMI_SOURCE
204+
198205
@property
199206
def brightness_range(self) -> tuple[int, int]:
200207
"""Get brightness min/max range. Default (0, 100)."""
@@ -291,6 +298,18 @@ def supports_work_mode(self) -> bool:
291298
"""Check if device supports work mode (fans)."""
292299
return any(cap.is_work_mode for cap in self.capabilities)
293300

301+
@property
302+
def supports_hdmi_source(self) -> bool:
303+
"""Check if device supports HDMI source selection."""
304+
return any(cap.is_hdmi_source for cap in self.capabilities)
305+
306+
def get_hdmi_source_options(self) -> list[dict[str, Any]]:
307+
"""Get available HDMI source options from capability parameters."""
308+
for cap in self.capabilities:
309+
if cap.is_hdmi_source:
310+
return cap.parameters.get("options", [])
311+
return []
312+
294313
@property
295314
def is_light_device(self) -> bool:
296315
"""Check if device is a light (not a plug, fan, or other appliance)."""

custom_components/govee/models/state.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ class GoveeDeviceState:
9393
work_mode: int | None = None # Fan work mode: 1=gearMode, 3=Auto, 9=Fan
9494
mode_value: int | None = None # Fan speed for gearMode: 1=Low, 2=Medium, 3=High
9595

96+
# HDMI source state (for devices like AI Sync Box H6604)
97+
hdmi_source: int | None = None # HDMI port: 1, 2, 3, 4
98+
9699
# Source tracking for state management
97100
# "api" = from REST poll, "mqtt" = from push, "optimistic" = from command
98101
source: str = "api"
@@ -142,6 +145,10 @@ def update_from_api(self, data: dict[str, Any]) -> None:
142145
self.work_mode = value.get("workMode")
143146
self.mode_value = value.get("modeValue")
144147

148+
elif cap_type == "devices.capabilities.mode":
149+
if instance == "hdmiSource":
150+
self.hdmi_source = int(value) if value is not None else None
151+
145152
def update_from_mqtt(self, data: dict[str, Any]) -> None:
146153
"""Update state from MQTT push message.
147154
@@ -220,6 +227,11 @@ def apply_optimistic_work_mode(self, work_mode: int, mode_value: int) -> None:
220227
self.mode_value = mode_value
221228
self.source = "optimistic"
222229

230+
def apply_optimistic_hdmi_source(self, source: int) -> None:
231+
"""Apply optimistic HDMI source update."""
232+
self.hdmi_source = source
233+
self.source = "optimistic"
234+
223235
@classmethod
224236
def create_empty(cls, device_id: str) -> GoveeDeviceState:
225237
"""Create empty state for a device."""

custom_components/govee/select.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
DOMAIN,
2626
)
2727
from .coordinator import GoveeCoordinator
28-
from .models import DIYSceneCommand, GoveeDevice, SceneCommand
28+
from .models import DIYSceneCommand, GoveeDevice, ModeCommand, SceneCommand
29+
from .models.device import INSTANCE_HDMI_SOURCE
2930

3031
# DIY Style options for select entity
3132
DIY_STYLE_OPTIONS = list(DIY_STYLE_NAMES.keys())
@@ -103,6 +104,19 @@ async def async_setup_entry(
103104
)
104105
_LOGGER.debug("Created DIY style select entity for %s", device.name)
105106

107+
# HDMI source selector (for devices like AI Sync Box H6604)
108+
if device.supports_hdmi_source:
109+
hdmi_options = device.get_hdmi_source_options()
110+
if hdmi_options:
111+
entities.append(
112+
GoveeHdmiSourceSelectEntity(
113+
coordinator=coordinator,
114+
device=device,
115+
options=hdmi_options,
116+
)
117+
)
118+
_LOGGER.debug("Created HDMI source select entity for %s", device.name)
119+
106120
async_add_entities(entities)
107121
_LOGGER.debug("Set up %d Govee scene select entities", len(entities))
108122

@@ -436,3 +450,114 @@ async def async_select_option(self, option: str) -> None:
436450
option,
437451
self._device.name,
438452
)
453+
454+
455+
class GoveeHdmiSourceSelectEntity(CoordinatorEntity["GoveeCoordinator"], SelectEntity):
456+
"""Govee HDMI source select entity.
457+
458+
Provides a dropdown to select HDMI input source on devices like
459+
the Govee AI Sync Box (H6604).
460+
"""
461+
462+
_attr_has_entity_name = True
463+
_attr_translation_key = "govee_hdmi_source_select"
464+
_attr_icon = "mdi:hdmi-port"
465+
466+
def __init__(
467+
self,
468+
coordinator: GoveeCoordinator,
469+
device: GoveeDevice,
470+
options: list[dict[str, Any]],
471+
) -> None:
472+
"""Initialize the HDMI source select entity.
473+
474+
Args:
475+
coordinator: Govee data coordinator.
476+
device: Device this select belongs to.
477+
options: List of HDMI source options from capability parameters.
478+
"""
479+
super().__init__(coordinator)
480+
481+
self._device = device
482+
self._device_id = device.device_id
483+
484+
# Build option mapping: display name -> value
485+
self._option_map: dict[str, int] = {}
486+
option_names: list[str] = []
487+
488+
for opt in options:
489+
name = opt.get("name", "")
490+
value = opt.get("value")
491+
if name and value is not None:
492+
self._option_map[name] = value
493+
option_names.append(name)
494+
495+
self._attr_options = option_names
496+
497+
# Unique ID
498+
self._attr_unique_id = f"{device.device_id}_hdmi_source_select"
499+
500+
# Entity name
501+
self._attr_name = "HDMI Source"
502+
503+
@property
504+
def device_info(self) -> DeviceInfo:
505+
"""Return device information."""
506+
return DeviceInfo(
507+
identifiers={(DOMAIN, self._device.device_id)},
508+
name=self._device.name,
509+
manufacturer="Govee",
510+
model=self._device.sku,
511+
)
512+
513+
@property
514+
def available(self) -> bool:
515+
"""Return True if entity is available."""
516+
state = self.coordinator.get_state(self._device_id)
517+
if state is None:
518+
return False
519+
return state.online or self._device.is_group
520+
521+
@property
522+
def current_option(self) -> str | None:
523+
"""Return current selected option from state."""
524+
state = self.coordinator.get_state(self._device_id)
525+
if state and state.hdmi_source is not None:
526+
# Find option name matching the current value
527+
for name, value in self._option_map.items():
528+
if value == state.hdmi_source:
529+
return name
530+
# Return first option as default if available
531+
return self._attr_options[0] if self._attr_options else None
532+
533+
async def async_select_option(self, option: str) -> None:
534+
"""Handle HDMI source selection."""
535+
value = self._option_map.get(option)
536+
if value is None:
537+
_LOGGER.warning("Unknown HDMI source option: %s", option)
538+
return
539+
540+
command = ModeCommand(
541+
mode_instance=INSTANCE_HDMI_SOURCE,
542+
value=value,
543+
)
544+
545+
success = await self.coordinator.async_control_device(
546+
self._device_id,
547+
command,
548+
)
549+
550+
if success:
551+
self.async_write_ha_state()
552+
_LOGGER.debug(
553+
"Set HDMI source '%s' (value=%d) on %s",
554+
option,
555+
value,
556+
self._device.name,
557+
)
558+
else:
559+
_LOGGER.warning(
560+
"Failed to set HDMI source '%s' on %s",
561+
option,
562+
self._device.name,
563+
)

0 commit comments

Comments
 (0)