Files
core/homeassistant/components/control4/__init__.py
Ville Skyttä b4bac0f7a0 Exception chaining and wrapping improvements (#39320)
* 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.
2020-08-28 13:50:32 +02:00

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()