mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add ESPHome User-Defined Services (#21409)
* Add ESPHome User-Defined Services * Update requirements_all.txt
This commit is contained in:
parent
d3f1ee4a89
commit
db4c06c8fe
@ -1,7 +1,7 @@
|
||||
"""Support for esphome devices."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable, Tuple
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
@ -13,6 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \
|
||||
from homeassistant.core import callback, Event, State
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect, \
|
||||
async_dispatcher_send
|
||||
@ -28,9 +29,10 @@ from .config_flow import EsphomeFlowHandler # noqa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \
|
||||
ServiceCall
|
||||
ServiceCall, UserService
|
||||
|
||||
REQUIREMENTS = ['aioesphomeapi==1.5.0']
|
||||
DOMAIN = 'esphome'
|
||||
REQUIREMENTS = ['aioesphomeapi==1.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -69,6 +71,7 @@ class RuntimeEntryData:
|
||||
reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None)
|
||||
state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
|
||||
info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
|
||||
services = attr.ib(type=Dict[int, 'UserService'], factory=dict)
|
||||
available = attr.ib(type=bool, default=False)
|
||||
device_info = attr.ib(type='DeviceInfo', default=None)
|
||||
cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list)
|
||||
@ -105,14 +108,16 @@ class RuntimeEntryData:
|
||||
signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id)
|
||||
async_dispatcher_send(hass, signal)
|
||||
|
||||
async def async_load_from_store(self) -> List['EntityInfo']:
|
||||
async def async_load_from_store(self) -> Tuple[List['EntityInfo'],
|
||||
List['UserService']]:
|
||||
"""Load the retained data from store and return de-serialized data."""
|
||||
# pylint: disable= redefined-outer-name
|
||||
from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo
|
||||
from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo, \
|
||||
UserService
|
||||
|
||||
restored = await self.store.async_load()
|
||||
if restored is None:
|
||||
return []
|
||||
return [], []
|
||||
|
||||
self.device_info = _attr_obj_from_dict(DeviceInfo,
|
||||
**restored.pop('device_info'))
|
||||
@ -123,17 +128,23 @@ class RuntimeEntryData:
|
||||
for info in restored_infos:
|
||||
cls = COMPONENT_TYPE_TO_INFO[comp_type]
|
||||
infos.append(_attr_obj_from_dict(cls, **info))
|
||||
return infos
|
||||
services = []
|
||||
for service in restored.get('services', []):
|
||||
services.append(UserService.from_dict(service))
|
||||
return infos, services
|
||||
|
||||
async def async_save_to_store(self) -> None:
|
||||
"""Generate dynamic data to store and save it to the filesystem."""
|
||||
store_data = {
|
||||
'device_info': attr.asdict(self.device_info)
|
||||
'device_info': attr.asdict(self.device_info),
|
||||
'services': []
|
||||
}
|
||||
|
||||
for comp_type, infos in self.info.items():
|
||||
store_data[comp_type] = [attr.asdict(info)
|
||||
for info in infos.values()]
|
||||
for service in self.services.values():
|
||||
store_data['services'].append(service.to_dict())
|
||||
|
||||
await self.store.async_save(store_data)
|
||||
|
||||
@ -233,8 +244,9 @@ async def async_setup_entry(hass: HomeAssistantType,
|
||||
entry_data.device_info)
|
||||
entry_data.async_update_device_state(hass)
|
||||
|
||||
entity_infos = await cli.list_entities()
|
||||
entity_infos, services = await cli.list_entities_services()
|
||||
entry_data.async_update_static_infos(hass, entity_infos)
|
||||
await _setup_services(hass, entry_data, services)
|
||||
await cli.subscribe_states(async_on_state)
|
||||
await cli.subscribe_service_calls(async_on_service_call)
|
||||
await cli.subscribe_home_assistant_states(
|
||||
@ -277,8 +289,9 @@ async def async_setup_entry(hass: HomeAssistantType,
|
||||
entry, component))
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
infos = await entry_data.async_load_from_store()
|
||||
infos, services = await entry_data.async_load_from_store()
|
||||
entry_data.async_update_static_infos(hass, infos)
|
||||
await _setup_services(hass, entry_data, services)
|
||||
|
||||
# If first connect fails, the next re-connect will be scheduled
|
||||
# outside of _pending_task, in order not to delay HA startup
|
||||
@ -366,6 +379,60 @@ async def _async_setup_device_registry(hass: HomeAssistantType,
|
||||
)
|
||||
|
||||
|
||||
async def _register_service(hass: HomeAssistantType,
|
||||
entry_data: RuntimeEntryData,
|
||||
service: 'UserService'):
|
||||
from aioesphomeapi import USER_SERVICE_ARG_BOOL, USER_SERVICE_ARG_INT, \
|
||||
USER_SERVICE_ARG_FLOAT, USER_SERVICE_ARG_STRING
|
||||
service_name = '{}_{}'.format(entry_data.device_info.name, service.name)
|
||||
schema = {}
|
||||
for arg in service.args:
|
||||
schema[vol.Required(arg.name)] = {
|
||||
USER_SERVICE_ARG_BOOL: cv.boolean,
|
||||
USER_SERVICE_ARG_INT: vol.Coerce(int),
|
||||
USER_SERVICE_ARG_FLOAT: vol.Coerce(float),
|
||||
USER_SERVICE_ARG_STRING: cv.string,
|
||||
}[arg.type_]
|
||||
|
||||
async def execute_service(call):
|
||||
await entry_data.client.execute_service(service, call.data)
|
||||
|
||||
hass.services.async_register(DOMAIN, service_name, execute_service,
|
||||
vol.Schema(schema))
|
||||
|
||||
|
||||
async def _setup_services(hass: HomeAssistantType,
|
||||
entry_data: RuntimeEntryData,
|
||||
services: List['UserService']):
|
||||
old_services = entry_data.services.copy()
|
||||
to_unregister = []
|
||||
to_register = []
|
||||
for service in services:
|
||||
if service.key in old_services:
|
||||
# Already exists
|
||||
matching = old_services.pop(service.key)
|
||||
if matching != service:
|
||||
# Need to re-register
|
||||
to_unregister.append(matching)
|
||||
to_register.append(service)
|
||||
else:
|
||||
# New service
|
||||
to_register.append(service)
|
||||
|
||||
for service in old_services.values():
|
||||
to_unregister.append(service)
|
||||
|
||||
entry_data.services = {serv.key: serv for serv in services}
|
||||
|
||||
for service in to_unregister:
|
||||
service_name = '{}_{}'.format(entry_data.device_info.name,
|
||||
service.name)
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
for service in to_register:
|
||||
await _register_service(hass, entry_data, service)
|
||||
|
||||
|
||||
async def _cleanup_instance(hass: HomeAssistantType,
|
||||
entry: ConfigEntry) -> None:
|
||||
"""Cleanup the esphome client if it exists."""
|
||||
|
@ -102,7 +102,7 @@ aioautomatic==0.6.5
|
||||
aiodns==1.1.1
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==1.5.0
|
||||
aioesphomeapi==1.6.0
|
||||
|
||||
# homeassistant.components.freebox
|
||||
aiofreepybox==0.0.6
|
||||
|
Loading…
x
Reference in New Issue
Block a user