usda-hass-config/custom_components/remote_homeassistant/proxy_services.py

108 lines
4.0 KiB
Python

"""Support for proxy services."""
import asyncio
import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service import SERVICE_DESCRIPTION_CACHE
from .const import CONF_SERVICE_PREFIX, CONF_SERVICES, SERVICE_CALL_LIMIT
class ProxyServices:
"""Manages remote proxy services."""
def __init__(self, hass, entry, remote):
"""Initialize a new ProxyServices instance."""
self.hass = hass
self.entry = entry
self.remote = remote
self.remote_services = {}
self.registered_services = []
@property
def services(self):
"""Return list of service names."""
result = []
for domain, services in self.remote_services.items():
for service in services.keys():
result.append(f"{domain}.{service}")
return sorted(result)
async def load(self):
"""Call to make initial registration of services."""
await self.remote.call(self._async_got_services, "get_services")
async def unload(self):
"""Call to unregister all registered services."""
description_cache = self.hass.data[SERVICE_DESCRIPTION_CACHE]
for domain, service_name in self.registered_services:
self.hass.services.async_remove(domain, service_name)
# Remove from internal description cache
service = f"{domain}.{service_name}"
if service in description_cache:
del description_cache[service]
async def _async_got_services(self, message):
"""Called when list of remote services is available."""
self.remote_services = message["result"]
# A service prefix is needed to not clash with original service names
service_prefix = self.entry.options.get(CONF_SERVICE_PREFIX)
if not service_prefix:
return
description_cache = self.hass.data[SERVICE_DESCRIPTION_CACHE]
for service in self.entry.options.get(CONF_SERVICES, []):
domain, service_name = service.split(".")
service = service_prefix + service_name
# Register new service with same name as original service but with prefix
self.hass.services.async_register(
domain,
service,
self._async_handle_service_call,
vol.Schema({}, extra=vol.ALLOW_EXTRA),
)
# <HERE_BE_DRAGON>
# Service metadata can only be provided via a services.yaml file for a
# particular component, something not possible here. A cache is used
# internally for loaded service descriptions and that's abused here. If
# the internal representation of the cache change, this sill break.
# </HERE_BE_DRAGONS>
service_info = self.remote_services.get(domain, {}).get(service_name)
if service_info:
description_cache[f"{domain}.{service}"] = service_info
self.registered_services.append((domain, service))
async def _async_handle_service_call(self, event):
"""Handle service call to proxy service."""
# An eception must be raised from the service call handler (thus method) in
# order to end up in the frontend. The code below synchronizes reception of
# the service call result, so potential error message can be used as exception
# message. Not very pretty...
ev = asyncio.Event()
res = None
def _resp(message):
nonlocal res
res = message
ev.set()
service_prefix = self.entry.options.get(CONF_SERVICE_PREFIX)
service = event.service[len(service_prefix) :]
await self.remote.call(
_resp,
"call_service",
domain=event.domain,
service=service,
service_data=event.data.copy(),
)
await asyncio.wait_for(ev.wait(), SERVICE_CALL_LIMIT)
if not res["success"]:
raise HomeAssistantError(res["error"]["message"])