Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for exposing regular REST commands as services."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 from json.decoder import JSONDecodeError
7 import logging
8 from typing import Any
9 
10 import aiohttp
11 from aiohttp import hdrs
12 import voluptuous as vol
13 
14 from homeassistant.const import (
15  CONF_HEADERS,
16  CONF_METHOD,
17  CONF_PASSWORD,
18  CONF_PAYLOAD,
19  CONF_TIMEOUT,
20  CONF_URL,
21  CONF_USERNAME,
22  CONF_VERIFY_SSL,
23  SERVICE_RELOAD,
24 )
25 from homeassistant.core import (
26  HomeAssistant,
27  ServiceCall,
28  ServiceResponse,
29  SupportsResponse,
30  callback,
31 )
32 from homeassistant.exceptions import HomeAssistantError
33 from homeassistant.helpers.aiohttp_client import async_get_clientsession
35 from homeassistant.helpers.reload import async_integration_yaml_config
36 from homeassistant.helpers.typing import ConfigType
37 
38 DOMAIN = "rest_command"
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 DEFAULT_TIMEOUT = 10
43 DEFAULT_METHOD = "get"
44 DEFAULT_VERIFY_SSL = True
45 
46 SUPPORT_REST_METHODS = ["get", "patch", "post", "put", "delete"]
47 
48 CONF_CONTENT_TYPE = "content_type"
49 
50 COMMAND_SCHEMA = vol.Schema(
51  {
52  vol.Required(CONF_URL): cv.template,
53  vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All(
54  vol.Lower, vol.In(SUPPORT_REST_METHODS)
55  ),
56  vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}),
57  vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
58  vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
59  vol.Optional(CONF_PAYLOAD): cv.template,
60  vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
61  vol.Optional(CONF_CONTENT_TYPE): cv.string,
62  vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
63  }
64 )
65 
66 CONFIG_SCHEMA = vol.Schema(
67  {DOMAIN: cv.schema_with_slug_keys(COMMAND_SCHEMA)}, extra=vol.ALLOW_EXTRA
68 )
69 
70 
71 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
72  """Set up the REST command component."""
73 
74  async def reload_service_handler(service: ServiceCall) -> None:
75  """Remove all rest_commands and load new ones from config."""
76  conf = await async_integration_yaml_config(hass, DOMAIN)
77 
78  # conf will be None if the configuration can't be parsed
79  if conf is None:
80  return
81 
82  existing = hass.services.async_services_for_domain(DOMAIN)
83  for existing_service in existing:
84  if existing_service == SERVICE_RELOAD:
85  continue
86  hass.services.async_remove(DOMAIN, existing_service)
87 
88  for name, command_config in conf[DOMAIN].items():
89  async_register_rest_command(name, command_config)
90 
91  @callback
92  def async_register_rest_command(name: str, command_config: dict[str, Any]) -> None:
93  """Create service for rest command."""
94  websession = async_get_clientsession(hass, command_config[CONF_VERIFY_SSL])
95  timeout = command_config[CONF_TIMEOUT]
96  method = command_config[CONF_METHOD]
97 
98  template_url = command_config[CONF_URL]
99 
100  auth = None
101  if CONF_USERNAME in command_config:
102  username = command_config[CONF_USERNAME]
103  password = command_config.get(CONF_PASSWORD, "")
104  auth = aiohttp.BasicAuth(username, password=password)
105 
106  template_payload = None
107  if CONF_PAYLOAD in command_config:
108  template_payload = command_config[CONF_PAYLOAD]
109 
110  template_headers = command_config.get(CONF_HEADERS, {})
111 
112  content_type = command_config.get(CONF_CONTENT_TYPE)
113 
114  async def async_service_handler(service: ServiceCall) -> ServiceResponse:
115  """Execute a shell command service."""
116  payload = None
117  if template_payload:
118  payload = bytes(
119  template_payload.async_render(
120  variables=service.data, parse_result=False
121  ),
122  "utf-8",
123  )
124 
125  request_url = template_url.async_render(
126  variables=service.data, parse_result=False
127  )
128 
129  headers = {}
130  for header_name, template_header in template_headers.items():
131  headers[header_name] = template_header.async_render(
132  variables=service.data, parse_result=False
133  )
134 
135  if content_type:
136  headers[hdrs.CONTENT_TYPE] = content_type
137 
138  try:
139  async with getattr(websession, method)(
140  request_url,
141  data=payload,
142  auth=auth,
143  headers=headers or None,
144  timeout=timeout,
145  ) as response:
146  if response.status < HTTPStatus.BAD_REQUEST:
147  _LOGGER.debug(
148  "Success. Url: %s. Status code: %d. Payload: %s",
149  response.url,
150  response.status,
151  payload,
152  )
153  else:
154  _LOGGER.warning(
155  "Error. Url: %s. Status code %d. Payload: %s",
156  response.url,
157  response.status,
158  payload,
159  )
160 
161  if not service.return_response:
162  return None
163 
164  _content = None
165  try:
166  if response.content_type == "application/json":
167  _content = await response.json()
168  else:
169  _content = await response.text()
170  except (JSONDecodeError, AttributeError) as err:
171  raise HomeAssistantError(
172  translation_domain=DOMAIN,
173  translation_key="decoding_error",
174  translation_placeholders={
175  "request_url": request_url,
176  "decoding_type": "JSON",
177  },
178  ) from err
179 
180  except UnicodeDecodeError as err:
181  raise HomeAssistantError(
182  translation_domain=DOMAIN,
183  translation_key="decoding_error",
184  translation_placeholders={
185  "request_url": request_url,
186  "decoding_type": "text",
187  },
188  ) from err
189  return {"content": _content, "status": response.status}
190 
191  except TimeoutError as err:
192  raise HomeAssistantError(
193  translation_domain=DOMAIN,
194  translation_key="timeout",
195  translation_placeholders={"request_url": request_url},
196  ) from err
197 
198  except aiohttp.ClientError as err:
199  _LOGGER.error("Error fetching data: %s", err)
200  raise HomeAssistantError(
201  translation_domain=DOMAIN,
202  translation_key="client_error",
203  translation_placeholders={"request_url": request_url},
204  ) from err
205 
206  # register services
207  hass.services.async_register(
208  DOMAIN,
209  name,
210  async_service_handler,
211  supports_response=SupportsResponse.OPTIONAL,
212  )
213 
214  for name, command_config in config[DOMAIN].items():
215  async_register_rest_command(name, command_config)
216 
217  hass.services.async_register(
218  DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
219  )
220 
221  return True
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:71
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
ConfigType|None async_integration_yaml_config(HomeAssistant hass, str integration_name)
Definition: reload.py:142