* Remove unnecessary exception re-wraps * Preserve exception chains on re-raise We slap "from cause" to almost all possible cases here. In some cases it could conceivably be better to do "from None" if we really want to hide the cause. However those should be in the minority, and "from cause" should be an improvement over the corresponding raise without a "from" in all cases anyway. The only case where we raise from None here is in plex, where the exception for an original invalid SSL cert is not the root cause for failure to validate a newly fetched one. Follow local convention on exception variable names if there is a consistent one, otherwise `err` to match with majority of codebase. * Fix mistaken re-wrap in homematicip_cloud/hap.py Missed the difference between HmipConnectionError and HmipcConnectionError. * Do not hide original error on plex new cert validation error Original is not the cause for the new one, but showing old in the traceback is useful nevertheless.
225 lines
7.2 KiB
Python
225 lines
7.2 KiB
Python
"""The Control4 integration."""
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
|
|
from aiohttp import client_exceptions
|
|
from pyControl4.account import C4Account
|
|
from pyControl4.director import C4Director
|
|
from pyControl4.error_handling import BadCredentials
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
CONF_HOST,
|
|
CONF_PASSWORD,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_TOKEN,
|
|
CONF_USERNAME,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
from homeassistant.helpers import aiohttp_client, device_registry as dr, entity
|
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
|
|
from .const import (
|
|
CONF_ACCOUNT,
|
|
CONF_CONFIG_LISTENER,
|
|
CONF_CONTROLLER_UNIQUE_ID,
|
|
CONF_DIRECTOR,
|
|
CONF_DIRECTOR_ALL_ITEMS,
|
|
CONF_DIRECTOR_MODEL,
|
|
CONF_DIRECTOR_SW_VERSION,
|
|
CONF_DIRECTOR_TOKEN_EXPIRATION,
|
|
DEFAULT_SCAN_INTERVAL,
|
|
DOMAIN,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORMS = ["light"]
|
|
|
|
|
|
async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Stub to allow setting up this component.
|
|
|
|
Configuration through YAML is not supported at this time.
|
|
"""
|
|
hass.data.setdefault(DOMAIN, {})
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
"""Set up Control4 from a config entry."""
|
|
entry_data = hass.data[DOMAIN].setdefault(entry.entry_id, {})
|
|
account_session = aiohttp_client.async_get_clientsession(hass)
|
|
|
|
config = entry.data
|
|
account = C4Account(config[CONF_USERNAME], config[CONF_PASSWORD], account_session)
|
|
try:
|
|
await account.getAccountBearerToken()
|
|
except client_exceptions.ClientError as exception:
|
|
_LOGGER.error("Error connecting to Control4 account API: %s", exception)
|
|
raise ConfigEntryNotReady from exception
|
|
except BadCredentials as exception:
|
|
_LOGGER.error(
|
|
"Error authenticating with Control4 account API, incorrect username or password: %s",
|
|
exception,
|
|
)
|
|
return False
|
|
entry_data[CONF_ACCOUNT] = account
|
|
|
|
controller_unique_id = config[CONF_CONTROLLER_UNIQUE_ID]
|
|
entry_data[CONF_CONTROLLER_UNIQUE_ID] = controller_unique_id
|
|
|
|
director_token_dict = await account.getDirectorBearerToken(controller_unique_id)
|
|
director_session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False)
|
|
|
|
director = C4Director(
|
|
config[CONF_HOST], director_token_dict[CONF_TOKEN], director_session
|
|
)
|
|
entry_data[CONF_DIRECTOR] = director
|
|
entry_data[CONF_DIRECTOR_TOKEN_EXPIRATION] = director_token_dict["token_expiration"]
|
|
|
|
# Add Control4 controller to device registry
|
|
controller_href = (await account.getAccountControllers())["href"]
|
|
entry_data[CONF_DIRECTOR_SW_VERSION] = await account.getControllerOSVersion(
|
|
controller_href
|
|
)
|
|
|
|
_, model, mac_address = controller_unique_id.split("_", 3)
|
|
entry_data[CONF_DIRECTOR_MODEL] = model.upper()
|
|
|
|
device_registry = await dr.async_get_registry(hass)
|
|
device_registry.async_get_or_create(
|
|
config_entry_id=entry.entry_id,
|
|
identifiers={(DOMAIN, controller_unique_id)},
|
|
connections={(dr.CONNECTION_NETWORK_MAC, mac_address)},
|
|
manufacturer="Control4",
|
|
name=controller_unique_id,
|
|
model=entry_data[CONF_DIRECTOR_MODEL],
|
|
sw_version=entry_data[CONF_DIRECTOR_SW_VERSION],
|
|
)
|
|
|
|
# Store all items found on controller for platforms to use
|
|
director_all_items = await director.getAllItemInfo()
|
|
director_all_items = json.loads(director_all_items)
|
|
entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items
|
|
|
|
# Load options from config entry
|
|
entry_data[CONF_SCAN_INTERVAL] = entry.options.get(
|
|
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
|
)
|
|
|
|
entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener)
|
|
|
|
for component in PLATFORMS:
|
|
hass.async_create_task(
|
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def update_listener(hass, config_entry):
|
|
"""Update when config_entry options update."""
|
|
_LOGGER.debug("Config entry was updated, rerunning setup")
|
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|
"""Unload a config entry."""
|
|
unload_ok = all(
|
|
await asyncio.gather(
|
|
*[
|
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
|
for component in PLATFORMS
|
|
]
|
|
)
|
|
)
|
|
hass.data[DOMAIN][entry.entry_id][CONF_CONFIG_LISTENER]()
|
|
if unload_ok:
|
|
hass.data[DOMAIN].pop(entry.entry_id)
|
|
_LOGGER.debug("Unloaded entry for %s", entry.entry_id)
|
|
|
|
return unload_ok
|
|
|
|
|
|
async def get_items_of_category(hass: HomeAssistant, entry: ConfigEntry, category: str):
|
|
"""Return a list of all Control4 items with the specified category."""
|
|
director_all_items = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS]
|
|
return_list = []
|
|
for item in director_all_items:
|
|
if "categories" in item and category in item["categories"]:
|
|
return_list.append(item)
|
|
return return_list
|
|
|
|
|
|
class Control4Entity(entity.Entity):
|
|
"""Base entity for Control4."""
|
|
|
|
def __init__(
|
|
self,
|
|
entry_data: dict,
|
|
entry: ConfigEntry,
|
|
coordinator: DataUpdateCoordinator,
|
|
name: str,
|
|
idx: int,
|
|
device_name: str,
|
|
device_manufacturer: str,
|
|
device_model: str,
|
|
device_id: int,
|
|
):
|
|
"""Initialize a Control4 entity."""
|
|
self.entry = entry
|
|
self.entry_data = entry_data
|
|
self._name = name
|
|
self._idx = idx
|
|
self._coordinator = coordinator
|
|
self._controller_unique_id = entry_data[CONF_CONTROLLER_UNIQUE_ID]
|
|
self._device_name = device_name
|
|
self._device_manufacturer = device_manufacturer
|
|
self._device_model = device_model
|
|
self._device_id = device_id
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return name of entity."""
|
|
return self._name
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique ID."""
|
|
return self._idx
|
|
|
|
@property
|
|
def device_info(self):
|
|
"""Return info of parent Control4 device of entity."""
|
|
return {
|
|
"config_entry_id": self.entry.entry_id,
|
|
"identifiers": {(DOMAIN, self._device_id)},
|
|
"name": self._device_name,
|
|
"manufacturer": self._device_manufacturer,
|
|
"model": self._device_model,
|
|
"via_device": (DOMAIN, self._controller_unique_id),
|
|
}
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""No need to poll. Coordinator notifies entity of updates."""
|
|
return False
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return if entity is available."""
|
|
return self._coordinator.last_update_success
|
|
|
|
async def async_added_to_hass(self):
|
|
"""When entity is added to hass."""
|
|
self.async_on_remove(
|
|
self._coordinator.async_add_listener(self.async_write_ha_state)
|
|
)
|
|
|
|
async def async_update(self):
|
|
"""Update the state of the device."""
|
|
await self._coordinator.async_request_refresh()
|