1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00
ha-core/homeassistant/components/system_bridge/__init__.py
2022-08-26 11:02:10 +02:00

312 lines
10 KiB
Python

"""The System Bridge integration."""
from __future__ import annotations
import asyncio
import logging
import async_timeout
from systembridgeconnector.exceptions import (
AuthenticationException,
ConnectionClosedException,
ConnectionErrorException,
)
from systembridgeconnector.models.keyboard_key import KeyboardKey
from systembridgeconnector.models.keyboard_text import KeyboardText
from systembridgeconnector.models.open_path import OpenPath
from systembridgeconnector.models.open_url import OpenUrl
from systembridgeconnector.version import SUPPORTED_VERSION, Version
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_PATH,
CONF_PORT,
CONF_URL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MODULES
from .coordinator import SystemBridgeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
]
CONF_BRIDGE = "bridge"
CONF_KEY = "key"
CONF_TEXT = "text"
SERVICE_OPEN_PATH = "open_path"
SERVICE_OPEN_URL = "open_url"
SERVICE_SEND_KEYPRESS = "send_keypress"
SERVICE_SEND_TEXT = "send_text"
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up System Bridge from a config entry."""
# Check version before initialising
version = Version(
entry.data[CONF_HOST],
entry.data[CONF_PORT],
entry.data[CONF_API_KEY],
session=async_get_clientsession(hass),
)
try:
if not await version.check_supported():
raise ConfigEntryNotReady(
f"You are not running a supported version of System Bridge. Please update to {SUPPORTED_VERSION} or higher."
)
except AuthenticationException as exception:
_LOGGER.error("Authentication failed for %s: %s", entry.title, exception)
raise ConfigEntryAuthFailed from exception
except (ConnectionClosedException, ConnectionErrorException) as exception:
raise ConfigEntryNotReady(
f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})."
) from exception
except asyncio.TimeoutError as exception:
raise ConfigEntryNotReady(
f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})."
) from exception
coordinator = SystemBridgeDataUpdateCoordinator(
hass,
_LOGGER,
entry=entry,
)
try:
async with async_timeout.timeout(30):
await coordinator.async_get_data(MODULES)
except AuthenticationException as exception:
_LOGGER.error("Authentication failed for %s: %s", entry.title, exception)
raise ConfigEntryAuthFailed from exception
except (ConnectionClosedException, ConnectionErrorException) as exception:
raise ConfigEntryNotReady(
f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})."
) from exception
except asyncio.TimeoutError as exception:
raise ConfigEntryNotReady(
f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})."
) from exception
await coordinator.async_config_entry_first_refresh()
try:
# Wait for initial data
async with async_timeout.timeout(30):
while not coordinator.is_ready():
_LOGGER.debug(
"Waiting for initial data from %s (%s)",
entry.title,
entry.data[CONF_HOST],
)
await asyncio.sleep(1)
except asyncio.TimeoutError as exception:
raise ConfigEntryNotReady(
f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})."
) from exception
_LOGGER.debug(
"Initial coordinator data for %s (%s):\n%s",
entry.title,
entry.data[CONF_HOST],
coordinator.data.json(),
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL):
return True
def valid_device(device: str):
"""Check device is valid."""
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(device)
if device_entry is not None:
try:
return next(
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
)
except StopIteration as exception:
raise vol.Invalid from exception
raise vol.Invalid(f"Device {device} does not exist")
async def handle_open_path(call: ServiceCall) -> None:
"""Handle the open path service call."""
_LOGGER.info("Open: %s", call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
await coordinator.websocket_client.open_path(
OpenPath(path=call.data[CONF_PATH])
)
async def handle_open_url(call: ServiceCall) -> None:
"""Handle the open url service call."""
_LOGGER.info("Open: %s", call.data)
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
await coordinator.websocket_client.open_url(OpenUrl(url=call.data[CONF_URL]))
async def handle_send_keypress(call: ServiceCall) -> None:
"""Handle the send_keypress service call."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
await coordinator.websocket_client.keyboard_keypress(
KeyboardKey(key=call.data[CONF_KEY])
)
async def handle_send_text(call: ServiceCall) -> None:
"""Handle the send_keypress service call."""
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
call.data[CONF_BRIDGE]
]
await coordinator.websocket_client.keyboard_text(
KeyboardText(text=call.data[CONF_TEXT])
)
hass.services.async_register(
DOMAIN,
SERVICE_OPEN_PATH,
handle_open_path,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_PATH): cv.string,
},
),
)
hass.services.async_register(
DOMAIN,
SERVICE_OPEN_URL,
handle_open_url,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_URL): cv.string,
},
),
)
hass.services.async_register(
DOMAIN,
SERVICE_SEND_KEYPRESS,
handle_send_keypress,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_KEY): cv.string,
},
),
)
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT,
handle_send_text,
schema=vol.Schema(
{
vol.Required(CONF_BRIDGE): valid_device,
vol.Required(CONF_TEXT): cv.string,
},
),
)
# Reload entry when its updated.
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
# Ensure disconnected and cleanup stop sub
await coordinator.websocket_client.close()
if coordinator.unsub:
coordinator.unsub()
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH)
hass.services.async_remove(DOMAIN, SERVICE_OPEN_URL)
hass.services.async_remove(DOMAIN, SERVICE_SEND_KEYPRESS)
hass.services.async_remove(DOMAIN, SERVICE_SEND_TEXT)
return unload_ok
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(entry.entry_id)
class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]):
"""Defines a base System Bridge entity."""
def __init__(
self,
coordinator: SystemBridgeDataUpdateCoordinator,
api_port: int,
key: str,
name: str | None,
) -> None:
"""Initialize the System Bridge entity."""
super().__init__(coordinator)
self._hostname = coordinator.data.system.hostname
self._key = f"{self._hostname}_{key}"
self._name = f"{self._hostname} {name}"
self._configuration_url = (
f"http://{self._hostname}:{api_port}/app/settings.html"
)
self._mac_address = coordinator.data.system.mac_address
self._uuid = coordinator.data.system.uuid
self._version = coordinator.data.system.version
@property
def unique_id(self) -> str:
"""Return the unique ID for this entity."""
return self._key
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this System Bridge instance."""
return DeviceInfo(
configuration_url=self._configuration_url,
connections={(dr.CONNECTION_NETWORK_MAC, self._mac_address)},
identifiers={(DOMAIN, self._uuid)},
name=self._hostname,
sw_version=self._version,
)