Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

This is a Home Assistant custom component for Govee LED strips and devices. It integrates with the Govee API to control lights, switches, and monitor device status. The component is distributed via HACS (Home Assistant Community Store).

## Development Commands

### Testing and Linting
- `tox` - Run tests and linting for Python 3.12 and 3.13 environments
- `flake8 .` - Run style checks (configured in tox.ini and setup.cfg)
- `pytest` - Run unit tests
- `black .` - Format code with Black formatter
- `isort .` - Sort imports according to configuration

### Dependencies
- Development dependencies are in `requirements_test.txt`
- Runtime dependencies are specified in `manifest.json` under `requirements`

## Code Architecture

### Component Structure
The integration follows Home Assistant's custom component pattern:

- **`custom_components/govee/`** - Main integration directory
- `__init__.py` - Component setup, platform loading, and lifecycle management
- `config_flow.py` - Configuration flow for user setup
- `const.py` - Constants and configuration keys
- `light.py` - Light platform implementation
- `learning_storage.py` - Device learning and configuration storage
- `manifest.json` - Component metadata and dependencies

### Key Dependencies
- `govee-api-laggat==0.2.2` - Core Govee API client library
- `dacite==1.8.0` - Data structure conversion

### Integration Flow
1. User configures via config flow with API key
2. Component creates Govee API client with learning storage
3. Devices are discovered and registered as light entities
4. Learning storage manages device-specific settings in `config/govee_learning.yaml`

### Device Learning System
The component includes a learning system that auto-discovers device capabilities:
- Brightness ranges (0-100 vs 0-254)
- Power-on behavior for brightness changes
- Offline handling preferences

### Configuration Options
- **Disable Attribute Updates** - Allows disabling specific state updates from API or history
- **Offline Is Off** - Treats offline devices as off (useful for USB-powered devices)
- **Use Assumed State** - Controls state assumption behavior

## Code Style
- Line length: 88 characters (Black formatter standard)
- Import sorting with isort
- Flake8 linting with specific ignores for Black compatibility
- Type hints encouraged but not strictly enforced

## Testing
- Tests located in `tests/` directory
- Uses pytest with Home Assistant test framework
- Async testing with pytest-asyncio
- GitHub Actions run tests on Python 3.12 and 3.13

## Implementation Notes

### SSL Blocking Operations Fix
The `govee-api-laggat` library's `Govee.create()` method internally performs blocking SSL certificate verification (`load_verify_locations()`) which violates Home Assistant's asyncio guidelines. This integration includes a workaround that:

1. **Pre-creates SSL context** in a thread pool using `hass.async_add_executor_job()`
2. **Temporarily patches** `ssl.create_default_context()` to return the pre-created context
3. **Allows `Govee.create()`** to proceed without blocking the event loop
4. **Restores** the original SSL function after creation

This approach eliminates the SSL blocking warning while maintaining full compatibility with the external library. The implementation is in the `async_create_govee_safely()` function in `__init__.py`.

### pkg_resources Deprecation Warning
A deprecation warning may appear about `pkg_resources` being deprecated in favor of `importlib.metadata`. This warning comes from upstream dependencies (particularly the `google` package) and cannot be resolved at the integration level. The warning is harmless and will be resolved when upstream dependencies migrate to the new APIs.

## Git Subtree
The project includes the `python-govee-api` library as a git subtree in `.git-subtree/python-govee-api/`. Changes to the underlying API library should be made there and pushed to the separate repository.
32 changes: 28 additions & 4 deletions custom_components/govee/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""The Govee integration."""
import asyncio
import logging
import ssl
import certifi

from govee_api_laggat import Govee
import voluptuous as vol
Expand Down Expand Up @@ -41,6 +43,28 @@ def is_online(online: bool):
_LOGGER.warning(msg)


async def async_create_govee_safely(hass: HomeAssistant, api_key: str, learning_storage):
"""Create Govee instance with SSL context pre-created to avoid blocking."""
# Pre-create SSL context in executor to avoid blocking the event loop
def _create_ssl_context():
return ssl.create_default_context(cafile=certifi.where())

# Create SSL context in thread pool
ssl_context = await hass.async_add_executor_job(_create_ssl_context)

# Temporarily patch ssl.create_default_context to return our pre-created context
original_create_default_context = ssl.create_default_context
ssl.create_default_context = lambda *args, **kwargs: ssl_context

try:
# Now create the Govee instance - it will use our pre-created SSL context
hub = await Govee.create(api_key, learning_storage=learning_storage)
return hub
finally:
# Restore original function
ssl.create_default_context = original_create_default_context


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Govee from a config entry."""

Expand All @@ -50,8 +74,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
api_key = options.get(CONF_API_KEY, config.get(CONF_API_KEY, ""))

# Setup connection with devices/cloud
hub = await Govee.create(
api_key, learning_storage=GoveeLearningStorage(hass.config.config_dir)
# Use our safe wrapper to avoid SSL blocking operations
hub = await async_create_govee_safely(
hass, api_key, GoveeLearningStorage(hass.config.config_dir)
)
# keep reference for disposing
hass.data[DOMAIN] = {}
Expand All @@ -68,8 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
await async_unload_entry(hass, entry)
raise PlatformNotReady()

for component in PLATFORMS:
await hass.config_entries.async_forward_entry_setup(entry, component)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True

Expand Down
1 change: 0 additions & 1 deletion custom_components/govee/govee

This file was deleted.

9 changes: 7 additions & 2 deletions custom_components/govee/learning_storage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The Govee learned storage yaml file manager."""

import asyncio
from dataclasses import asdict
import logging

Expand All @@ -25,7 +26,9 @@ async def read(self):
"""Restore from yaml file."""
learned_info = {}
try:
learned_dict = load_yaml(self._config_dir + LEARNING_STORAGE_YAML)
learned_dict = await asyncio.to_thread(
load_yaml, self._config_dir + LEARNING_STORAGE_YAML
)
learned_info = {
device: dacite.from_dict(
data_class=GoveeLearnedInfo, data=learned_dict[device]
Expand Down Expand Up @@ -59,7 +62,9 @@ async def read(self):
async def write(self, learned_info):
"""Save to yaml file."""
leaned_dict = {device: asdict(learned_info[device]) for device in learned_info}
save_yaml(self._config_dir + LEARNING_STORAGE_YAML, leaned_dict)
await asyncio.to_thread(
save_yaml, self._config_dir + LEARNING_STORAGE_YAML, leaned_dict
)
_LOGGER.info(
"Stored learning information to %s.",
self._config_dir + LEARNING_STORAGE_YAML,
Expand Down