mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +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."""
|
"""Support for esphome devices."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
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 attr
|
||||||
import voluptuous as vol
|
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
|
from homeassistant.core import callback, Event, State
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect, \
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect, \
|
||||||
async_dispatcher_send
|
async_dispatcher_send
|
||||||
@ -28,9 +29,10 @@ from .config_flow import EsphomeFlowHandler # noqa
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -69,6 +71,7 @@ class RuntimeEntryData:
|
|||||||
reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None)
|
reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None)
|
||||||
state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
|
state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
|
||||||
info = 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)
|
available = attr.ib(type=bool, default=False)
|
||||||
device_info = attr.ib(type='DeviceInfo', default=None)
|
device_info = attr.ib(type='DeviceInfo', default=None)
|
||||||
cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list)
|
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)
|
signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id)
|
||||||
async_dispatcher_send(hass, signal)
|
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."""
|
"""Load the retained data from store and return de-serialized data."""
|
||||||
# pylint: disable= redefined-outer-name
|
# 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()
|
restored = await self.store.async_load()
|
||||||
if restored is None:
|
if restored is None:
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
self.device_info = _attr_obj_from_dict(DeviceInfo,
|
self.device_info = _attr_obj_from_dict(DeviceInfo,
|
||||||
**restored.pop('device_info'))
|
**restored.pop('device_info'))
|
||||||
@ -123,17 +128,23 @@ class RuntimeEntryData:
|
|||||||
for info in restored_infos:
|
for info in restored_infos:
|
||||||
cls = COMPONENT_TYPE_TO_INFO[comp_type]
|
cls = COMPONENT_TYPE_TO_INFO[comp_type]
|
||||||
infos.append(_attr_obj_from_dict(cls, **info))
|
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:
|
async def async_save_to_store(self) -> None:
|
||||||
"""Generate dynamic data to store and save it to the filesystem."""
|
"""Generate dynamic data to store and save it to the filesystem."""
|
||||||
store_data = {
|
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():
|
for comp_type, infos in self.info.items():
|
||||||
store_data[comp_type] = [attr.asdict(info)
|
store_data[comp_type] = [attr.asdict(info)
|
||||||
for info in infos.values()]
|
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)
|
await self.store.async_save(store_data)
|
||||||
|
|
||||||
@ -233,8 +244,9 @@ async def async_setup_entry(hass: HomeAssistantType,
|
|||||||
entry_data.device_info)
|
entry_data.device_info)
|
||||||
entry_data.async_update_device_state(hass)
|
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)
|
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_states(async_on_state)
|
||||||
await cli.subscribe_service_calls(async_on_service_call)
|
await cli.subscribe_service_calls(async_on_service_call)
|
||||||
await cli.subscribe_home_assistant_states(
|
await cli.subscribe_home_assistant_states(
|
||||||
@ -277,8 +289,9 @@ async def async_setup_entry(hass: HomeAssistantType,
|
|||||||
entry, component))
|
entry, component))
|
||||||
await asyncio.wait(tasks)
|
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)
|
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
|
# If first connect fails, the next re-connect will be scheduled
|
||||||
# outside of _pending_task, in order not to delay HA startup
|
# 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,
|
async def _cleanup_instance(hass: HomeAssistantType,
|
||||||
entry: ConfigEntry) -> None:
|
entry: ConfigEntry) -> None:
|
||||||
"""Cleanup the esphome client if it exists."""
|
"""Cleanup the esphome client if it exists."""
|
||||||
|
@ -102,7 +102,7 @@ aioautomatic==0.6.5
|
|||||||
aiodns==1.1.1
|
aiodns==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==1.5.0
|
aioesphomeapi==1.6.0
|
||||||
|
|
||||||
# homeassistant.components.freebox
|
# homeassistant.components.freebox
|
||||||
aiofreepybox==0.0.6
|
aiofreepybox==0.0.6
|
||||||
|
Loading…
x
Reference in New Issue
Block a user