Home Assistant Unofficial Reference 2024.12.1
switch.py
Go to the documentation of this file.
1 """Support for RESTful switches."""
2 
3 from __future__ import annotations
4 
5 from http import HTTPStatus
6 import logging
7 from typing import Any
8 
9 import httpx
10 import voluptuous as vol
11 
13  DEVICE_CLASSES_SCHEMA,
14  PLATFORM_SCHEMA as SWITCH_PLATFORM_SCHEMA,
15  SwitchEntity,
16 )
17 from homeassistant.const import (
18  CONF_DEVICE_CLASS,
19  CONF_HEADERS,
20  CONF_ICON,
21  CONF_METHOD,
22  CONF_NAME,
23  CONF_PARAMS,
24  CONF_PASSWORD,
25  CONF_RESOURCE,
26  CONF_TIMEOUT,
27  CONF_UNIQUE_ID,
28  CONF_USERNAME,
29  CONF_VERIFY_SSL,
30 )
31 from homeassistant.core import HomeAssistant
32 from homeassistant.exceptions import PlatformNotReady
33 from homeassistant.helpers import config_validation as cv, template
34 from homeassistant.helpers.entity_platform import AddEntitiesCallback
35 from homeassistant.helpers.httpx_client import get_async_client
37  CONF_AVAILABILITY,
38  CONF_PICTURE,
39  TEMPLATE_ENTITY_BASE_SCHEMA,
40  ManualTriggerEntity,
41 )
42 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
43 
44 _LOGGER = logging.getLogger(__name__)
45 CONF_BODY_OFF = "body_off"
46 CONF_BODY_ON = "body_on"
47 CONF_IS_ON_TEMPLATE = "is_on_template"
48 CONF_STATE_RESOURCE = "state_resource"
49 
50 TRIGGER_ENTITY_OPTIONS = (
51  CONF_AVAILABILITY,
52  CONF_DEVICE_CLASS,
53  CONF_ICON,
54  CONF_PICTURE,
55  CONF_UNIQUE_ID,
56 )
57 
58 DEFAULT_METHOD = "post"
59 DEFAULT_BODY_OFF = "OFF"
60 DEFAULT_BODY_ON = "ON"
61 DEFAULT_NAME = "REST Switch"
62 DEFAULT_TIMEOUT = 10
63 DEFAULT_VERIFY_SSL = True
64 
65 SUPPORT_REST_METHODS = ["post", "put", "patch"]
66 
67 PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
68  {
69  **TEMPLATE_ENTITY_BASE_SCHEMA.schema,
70  vol.Required(CONF_RESOURCE): cv.url,
71  vol.Optional(CONF_STATE_RESOURCE): cv.url,
72  vol.Optional(CONF_HEADERS): {cv.string: cv.template},
73  vol.Optional(CONF_PARAMS): {cv.string: cv.template},
74  vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template,
75  vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template,
76  vol.Optional(CONF_IS_ON_TEMPLATE): cv.template,
77  vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All(
78  vol.Lower, vol.In(SUPPORT_REST_METHODS)
79  ),
80  vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
81  vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
82  vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
83  vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
84  vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
85  vol.Optional(CONF_AVAILABILITY): cv.template,
86  }
87 )
88 
89 
91  hass: HomeAssistant,
92  config: ConfigType,
93  async_add_entities: AddEntitiesCallback,
94  discovery_info: DiscoveryInfoType | None = None,
95 ) -> None:
96  """Set up the RESTful switch."""
97  resource: str = config[CONF_RESOURCE]
98  name = config.get(CONF_NAME) or template.Template(DEFAULT_NAME, hass)
99 
100  trigger_entity_config = {CONF_NAME: name}
101 
102  for key in TRIGGER_ENTITY_OPTIONS:
103  if key not in config:
104  continue
105  trigger_entity_config[key] = config[key]
106 
107  try:
108  switch = RestSwitch(hass, config, trigger_entity_config)
109 
110  req = await switch.get_device_state(hass)
111  if req.status_code >= HTTPStatus.BAD_REQUEST:
112  _LOGGER.error("Got non-ok response from resource: %s", req.status_code)
113  else:
114  async_add_entities([switch])
115  except (TypeError, ValueError):
116  _LOGGER.error(
117  "Missing resource or schema in configuration. "
118  "Add http:// or https:// to your URL"
119  )
120  except (TimeoutError, httpx.RequestError) as exc:
121  raise PlatformNotReady(f"No route to resource/endpoint: {resource}") from exc
122 
123 
125  """Representation of a switch that can be toggled using REST."""
126 
127  def __init__(
128  self,
129  hass: HomeAssistant,
130  config: ConfigType,
131  trigger_entity_config: ConfigType,
132  ) -> None:
133  """Initialize the REST switch."""
134  ManualTriggerEntity.__init__(self, hass, trigger_entity_config)
135 
136  auth: httpx.BasicAuth | None = None
137  username: str | None = None
138  if username := config.get(CONF_USERNAME):
139  password: str = config[CONF_PASSWORD]
140  auth = httpx.BasicAuth(username, password=password)
141 
142  self._resource: str = config[CONF_RESOURCE]
143  self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource
144  self._method: str = config[CONF_METHOD]
145  self._headers: dict[str, template.Template] | None = config.get(CONF_HEADERS)
146  self._params: dict[str, template.Template] | None = config.get(CONF_PARAMS)
147  self._auth_auth = auth
148  self._body_on: template.Template = config[CONF_BODY_ON]
149  self._body_off: template.Template = config[CONF_BODY_OFF]
150  self._is_on_template: template.Template | None = config.get(CONF_IS_ON_TEMPLATE)
151  self._timeout: int = config[CONF_TIMEOUT]
152  self._verify_ssl: bool = config[CONF_VERIFY_SSL]
153 
154  async def async_added_to_hass(self) -> None:
155  """Handle adding to Home Assistant."""
156  await super().async_added_to_hass()
157  await self.async_updateasync_update()
158 
159  async def async_turn_on(self, **kwargs: Any) -> None:
160  """Turn the device on."""
161  body_on_t = self._body_on.async_render(parse_result=False)
162 
163  try:
164  req = await self.set_device_stateset_device_state(body_on_t)
165 
166  if HTTPStatus.OK <= req.status_code < HTTPStatus.MULTIPLE_CHOICES:
167  self._attr_is_on_attr_is_on = True
168  else:
169  _LOGGER.error(
170  "Can't turn on %s. Is resource/endpoint offline?", self._resource
171  )
172  except (TimeoutError, httpx.RequestError):
173  _LOGGER.error("Error while switching on %s", self._resource)
174 
175  async def async_turn_off(self, **kwargs: Any) -> None:
176  """Turn the device off."""
177  body_off_t = self._body_off.async_render(parse_result=False)
178 
179  try:
180  req = await self.set_device_stateset_device_state(body_off_t)
181  if HTTPStatus.OK <= req.status_code < HTTPStatus.MULTIPLE_CHOICES:
182  self._attr_is_on_attr_is_on = False
183  else:
184  _LOGGER.error(
185  "Can't turn off %s. Is resource/endpoint offline?", self._resource
186  )
187  except (TimeoutError, httpx.RequestError):
188  _LOGGER.error("Error while switching off %s", self._resource)
189 
190  async def set_device_state(self, body: Any) -> httpx.Response:
191  """Send a state update to the device."""
192  websession = get_async_client(self.hasshasshass, self._verify_ssl)
193 
194  rendered_headers = template.render_complex(self._headers, parse_result=False)
195  rendered_params = template.render_complex(self._params)
196 
197  req: httpx.Response = await getattr(websession, self._method)(
198  self._resource,
199  auth=self._auth_auth,
200  content=bytes(body, "utf-8"),
201  headers=rendered_headers,
202  params=rendered_params,
203  timeout=self._timeout,
204  )
205  return req
206 
207  async def async_update(self) -> None:
208  """Get the current state, catching errors."""
209  req = None
210  try:
211  req = await self.get_device_stateget_device_state(self.hasshasshass)
212  except (TimeoutError, httpx.TimeoutException):
213  _LOGGER.exception("Timed out while fetching data")
214  except httpx.RequestError:
215  _LOGGER.exception("Error while fetching data")
216 
217  if req:
218  self._process_manual_data_process_manual_data(req.text)
219  self.async_write_ha_stateasync_write_ha_state()
220 
221  async def get_device_state(self, hass: HomeAssistant) -> httpx.Response:
222  """Get the latest data from REST API and update the state."""
223  websession = get_async_client(hass, self._verify_ssl)
224 
225  rendered_headers = template.render_complex(self._headers, parse_result=False)
226  rendered_params = template.render_complex(self._params)
227 
228  req = await websession.get(
229  self._state_resource,
230  auth=self._auth_auth,
231  headers=rendered_headers,
232  params=rendered_params,
233  timeout=self._timeout,
234  )
235  text = req.text
236 
237  if self._is_on_template is not None:
238  text = self._is_on_template.async_render_with_possible_json_value(
239  text, "None"
240  )
241  text = text.lower()
242  if text == "true":
243  self._attr_is_on_attr_is_on = True
244  elif text == "false":
245  self._attr_is_on_attr_is_on = False
246  else:
247  self._attr_is_on_attr_is_on = None
248  elif text == self._body_on.template:
249  self._attr_is_on_attr_is_on = True
250  elif text == self._body_off.template:
251  self._attr_is_on_attr_is_on = False
252  else:
253  self._attr_is_on_attr_is_on = None
254 
255  return req
httpx.Response set_device_state(self, Any body)
Definition: switch.py:190
None __init__(self, HomeAssistant hass, ConfigType config, ConfigType trigger_entity_config)
Definition: switch.py:132
httpx.Response get_device_state(self, HomeAssistant hass)
Definition: switch.py:221
None async_turn_on(self, **Any kwargs)
Definition: switch.py:159
None async_turn_off(self, **Any kwargs)
Definition: switch.py:175
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: switch.py:95
httpx.AsyncClient get_async_client(HomeAssistant hass, bool verify_ssl=True)
Definition: httpx_client.py:41