diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9a0308f --- /dev/null +++ b/CLAUDE.md @@ -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. \ No newline at end of file diff --git a/custom_components/govee/__init__.py b/custom_components/govee/__init__.py index b92ceaa..89172b7 100644 --- a/custom_components/govee/__init__.py +++ b/custom_components/govee/__init__.py @@ -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 @@ -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.""" @@ -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] = {} @@ -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 diff --git a/custom_components/govee/govee b/custom_components/govee/govee deleted file mode 120000 index dc2b666..0000000 --- a/custom_components/govee/govee +++ /dev/null @@ -1 +0,0 @@ -/workspaces/hacs-govee/custom_components/govee \ No newline at end of file diff --git a/custom_components/govee/learning_storage.py b/custom_components/govee/learning_storage.py index f33ce3a..ec5b939 100644 --- a/custom_components/govee/learning_storage.py +++ b/custom_components/govee/learning_storage.py @@ -1,5 +1,6 @@ """The Govee learned storage yaml file manager.""" +import asyncio from dataclasses import asdict import logging @@ -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] @@ -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,