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
341 changes: 269 additions & 72 deletions README.md

Large diffs are not rendered by default.

167 changes: 93 additions & 74 deletions custom_components/govee/__init__.py
Original file line number Diff line number Diff line change
@@ -1,112 +1,131 @@
"""The Govee integration."""
import asyncio
import logging
from __future__ import annotations

from govee_api_laggat import Govee
import voluptuous as vol
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.const import CONF_API_KEY, CONF_DELAY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN
from .learning_storage import GoveeLearningStorage
from .api import GoveeApiClient, GoveeApiError, GoveeAuthError
from .const import CONFIG_ENTRY_VERSION, DEFAULT_POLL_INTERVAL, DOMAIN, PLATFORMS
from .coordinator import GoveeDataUpdateCoordinator
from .models import GoveeDevice

_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)

# supported platforms
PLATFORMS = ["light"]


def setup(hass, config):
"""This setup does nothing, we use the async setup."""
hass.states.set("govee.state", "setup called")
return True

@dataclass
class GoveeRuntimeData:
"""Runtime data for Govee integration."""

async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Govee component."""
hass.states.async_set("govee.state", "async_setup called")
hass.data[DOMAIN] = {}
return True
client: GoveeApiClient
coordinator: GoveeDataUpdateCoordinator
devices: dict[str, GoveeDevice]


def is_online(online: bool):
"""Log online/offline change."""
msg = "API is offline."
if online:
msg = "API is back online."
_LOGGER.warning(msg)
type GoveeConfigEntry = ConfigEntry[GoveeRuntimeData]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, entry: GoveeConfigEntry) -> bool:
"""Set up Govee from a config entry."""
_LOGGER.debug("Setting up Govee integration")

# get vars from ConfigFlow/OptionsFlow
# Get configuration
config = entry.data
options = entry.options
api_key = options.get(CONF_API_KEY, config.get(CONF_API_KEY, ""))
poll_interval = options.get(CONF_DELAY, config.get(CONF_DELAY, DEFAULT_POLL_INTERVAL))

# Create API client with shared session
session = async_get_clientsession(hass)
client = GoveeApiClient(api_key, session=session)

# Create coordinator
coordinator = GoveeDataUpdateCoordinator(
hass,
entry,
client,
update_interval=timedelta(seconds=poll_interval),
)

# Setup connection with devices/cloud
hub = await Govee.create(
api_key, learning_storage=GoveeLearningStorage(hass.config.config_dir)
# Fetch initial data
try:
await coordinator.async_config_entry_first_refresh()
except GoveeAuthError as err:
_LOGGER.error("Invalid API key: %s", err)
await client.close()
raise ConfigEntryNotReady("Invalid API key") from err
except GoveeApiError as err:
_LOGGER.error("Failed to connect to Govee API: %s", err)
await client.close()
raise ConfigEntryNotReady(f"Failed to connect: {err}") from err

# Store runtime data
entry.runtime_data = GoveeRuntimeData(
client=client,
coordinator=coordinator,
devices=coordinator.devices,
)
# keep reference for disposing
hass.data[DOMAIN] = {}
hass.data[DOMAIN]["hub"] = hub

# inform when api is offline/online
hub.events.online += is_online
# Set up platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

# Verify that passed in configuration works
_, err = await hub.get_devices()
if err:
_LOGGER.warning("Could not connect to Govee API: %s", err)
await hub.rate_limit_delay()
await async_unload_entry(hass, entry)
raise PlatformNotReady()
# Register update listener for options changes
entry.async_on_unload(entry.add_update_listener(async_options_updated))

for component in PLATFORMS:
await hass.config_entries.async_forward_entry_setups(entry, [component])
_LOGGER.info(
"Govee integration set up with %d devices",
len(coordinator.devices),
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: GoveeConfigEntry) -> bool:
"""Unload a config entry."""
_LOGGER.debug("Unloading Govee integration")

unload_ok = all(
await asyncio.gather(
*[
_unload_component_entry(hass, entry, component)
for component in PLATFORMS
]
)
)
# Unload platforms
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
hub = hass.data[DOMAIN].pop("hub")
await hub.close()
# Close API client (only if we own the session)
await entry.runtime_data.client.close()

return unload_ok


def _unload_component_entry(
hass: HomeAssistant, entry: ConfigEntry, component: str
) -> bool:
"""Unload an entry for a specific component."""
success = False
try:
success = hass.config_entries.async_forward_entry_unload(entry, component)
except ValueError:
# probably ValueError: Config entry was never loaded!
return success
except Exception as ex:
_LOGGER.warning(
"Continuing on exception when unloading %s component's entry: %s",
component,
ex,
async def async_options_updated(hass: HomeAssistant, entry: GoveeConfigEntry) -> None:
"""Handle options update."""
_LOGGER.debug("Options updated, reloading integration")
await hass.config_entries.async_reload(entry.entry_id)


async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry to new version."""
_LOGGER.debug("Migrating config entry from version %s", entry.version)

if entry.version < CONFIG_ENTRY_VERSION:
# Migration from v1 (govee-api-laggat) to v2 (inline API client)
new_data = dict(entry.data)
new_options = dict(entry.options)

# No data format changes needed for basic migration
# The main change is the underlying API client

hass.config_entries.async_update_entry(
entry,
data=new_data,
options=new_options,
version=CONFIG_ENTRY_VERSION,
)
return success

_LOGGER.info("Migration to version %s successful", CONFIG_ENTRY_VERSION)

return True
86 changes: 86 additions & 0 deletions custom_components/govee/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Govee API v2.0 client package."""
from __future__ import annotations

from .client import GoveeApiClient, RateLimiter
from .const import (
BASE_URL,
BRIGHTNESS_MAX,
BRIGHTNESS_MIN,
CAPABILITY_COLOR_SETTING,
CAPABILITY_DYNAMIC_SCENE,
CAPABILITY_MUSIC_SETTING,
CAPABILITY_ON_OFF,
CAPABILITY_RANGE,
CAPABILITY_SEGMENT_COLOR,
COLOR_TEMP_MAX,
COLOR_TEMP_MIN,
DEVICE_TYPE_AIR_PURIFIER,
DEVICE_TYPE_HEATER,
DEVICE_TYPE_HUMIDIFIER,
DEVICE_TYPE_LIGHT,
DEVICE_TYPE_SENSOR,
DEVICE_TYPE_SOCKET,
DEVICE_TYPE_THERMOMETER,
INSTANCE_BRIGHTNESS,
INSTANCE_COLOR_RGB,
INSTANCE_COLOR_TEMP,
INSTANCE_DIY_SCENE,
INSTANCE_LIGHT_SCENE,
INSTANCE_MUSIC_MODE,
INSTANCE_POWER_SWITCH,
INSTANCE_SEGMENTED_BRIGHTNESS,
INSTANCE_SEGMENTED_COLOR,
)
from .exceptions import (
GoveeApiError,
GoveeAuthError,
GoveeCapabilityError,
GoveeConnectionError,
GoveeDeviceError,
GoveeRateLimitError,
)

__all__ = [
# Client
"GoveeApiClient",
"RateLimiter",
# Exceptions
"GoveeApiError",
"GoveeAuthError",
"GoveeCapabilityError",
"GoveeConnectionError",
"GoveeDeviceError",
"GoveeRateLimitError",
# Constants - API
"BASE_URL",
# Constants - Capabilities
"CAPABILITY_COLOR_SETTING",
"CAPABILITY_DYNAMIC_SCENE",
"CAPABILITY_MUSIC_SETTING",
"CAPABILITY_ON_OFF",
"CAPABILITY_RANGE",
"CAPABILITY_SEGMENT_COLOR",
# Constants - Instances
"INSTANCE_BRIGHTNESS",
"INSTANCE_COLOR_RGB",
"INSTANCE_COLOR_TEMP",
"INSTANCE_DIY_SCENE",
"INSTANCE_LIGHT_SCENE",
"INSTANCE_MUSIC_MODE",
"INSTANCE_POWER_SWITCH",
"INSTANCE_SEGMENTED_BRIGHTNESS",
"INSTANCE_SEGMENTED_COLOR",
# Constants - Device Types
"DEVICE_TYPE_AIR_PURIFIER",
"DEVICE_TYPE_HEATER",
"DEVICE_TYPE_HUMIDIFIER",
"DEVICE_TYPE_LIGHT",
"DEVICE_TYPE_SENSOR",
"DEVICE_TYPE_SOCKET",
"DEVICE_TYPE_THERMOMETER",
# Constants - Ranges
"BRIGHTNESS_MAX",
"BRIGHTNESS_MIN",
"COLOR_TEMP_MAX",
"COLOR_TEMP_MIN",
]
Loading