Home Assistant Unofficial Reference 2024.12.1
type_switches.py
Go to the documentation of this file.
1 """Class to hold all switch accessories."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, Final, NamedTuple
7 
8 from pyhap.characteristic import Characteristic
9 from pyhap.const import (
10  CATEGORY_FAUCET,
11  CATEGORY_OUTLET,
12  CATEGORY_SHOWER_HEAD,
13  CATEGORY_SPRINKLER,
14  CATEGORY_SWITCH,
15 )
16 
17 from homeassistant.components import button, input_button
18 from homeassistant.components.input_select import ATTR_OPTIONS, SERVICE_SELECT_OPTION
19 from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
21  DOMAIN as VACUUM_DOMAIN,
22  SERVICE_RETURN_TO_BASE,
23  SERVICE_START,
24  STATE_CLEANING,
25  VacuumEntityFeature,
26 )
27 from homeassistant.const import (
28  ATTR_ENTITY_ID,
29  ATTR_SUPPORTED_FEATURES,
30  CONF_TYPE,
31  SERVICE_CLOSE_VALVE,
32  SERVICE_OPEN_VALVE,
33  SERVICE_TURN_OFF,
34  SERVICE_TURN_ON,
35  STATE_CLOSING,
36  STATE_ON,
37  STATE_OPEN,
38  STATE_OPENING,
39 )
40 from homeassistant.core import HomeAssistant, State, callback, split_entity_id
41 from homeassistant.helpers.event import async_call_later
42 
43 from .accessories import TYPES, HomeAccessory, HomeDriver
44 from .const import (
45  CHAR_ACTIVE,
46  CHAR_IN_USE,
47  CHAR_NAME,
48  CHAR_ON,
49  CHAR_OUTLET_IN_USE,
50  CHAR_VALVE_TYPE,
51  SERV_OUTLET,
52  SERV_SWITCH,
53  SERV_VALVE,
54  TYPE_FAUCET,
55  TYPE_SHOWER,
56  TYPE_SPRINKLER,
57  TYPE_VALVE,
58 )
59 from .util import cleanup_name_for_homekit
60 
61 _LOGGER = logging.getLogger(__name__)
62 
63 VALVE_OPEN_STATES: Final = {STATE_OPEN, STATE_OPENING, STATE_CLOSING}
64 
65 
66 class ValveInfo(NamedTuple):
67  """Category and type information for valve."""
68 
69  category: int
70  valve_type: int
71 
72 
73 VALVE_TYPE: dict[str, ValveInfo] = {
74  TYPE_FAUCET: ValveInfo(CATEGORY_FAUCET, 3),
75  TYPE_SHOWER: ValveInfo(CATEGORY_SHOWER_HEAD, 2),
76  TYPE_SPRINKLER: ValveInfo(CATEGORY_SPRINKLER, 1),
77  TYPE_VALVE: ValveInfo(CATEGORY_FAUCET, 0),
78 }
79 
80 
81 ACTIVATE_ONLY_SWITCH_DOMAINS = {"button", "input_button", "scene", "script"}
82 
83 ACTIVATE_ONLY_RESET_SECONDS = 10
84 
85 
86 @TYPES.register("Outlet")
88  """Generate an Outlet accessory."""
89 
90  def __init__(self, *args: Any) -> None:
91  """Initialize an Outlet accessory object."""
92  super().__init__(*args, category=CATEGORY_OUTLET)
93  state = self.hasshass.states.get(self.entity_identity_id)
94  assert state
95 
96  serv_outlet = self.add_preload_service(SERV_OUTLET)
97  self.char_onchar_on = serv_outlet.configure_char(
98  CHAR_ON, value=False, setter_callback=self.set_stateset_state
99  )
100  self.char_outlet_in_usechar_outlet_in_use = serv_outlet.configure_char(
101  CHAR_OUTLET_IN_USE, value=True
102  )
103  # Set the state so it is in sync on initial
104  # GET to avoid an event storm after homekit startup
105  self.async_update_stateasync_update_stateasync_update_state(state)
106 
107  def set_state(self, value: bool) -> None:
108  """Move switch state to value if call came from HomeKit."""
109  _LOGGER.debug("%s: Set switch state to %s", self.entity_identity_id, value)
110  params = {ATTR_ENTITY_ID: self.entity_identity_id}
111  service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
112  self.async_call_serviceasync_call_service(SWITCH_DOMAIN, service, params)
113 
114  @callback
115  def async_update_state(self, new_state: State) -> None:
116  """Update switch state after state changed."""
117  current_state = new_state.state == STATE_ON
118  _LOGGER.debug("%s: Set current state to %s", self.entity_identity_id, current_state)
119  self.char_onchar_on.set_value(current_state)
120 
121 
122 @TYPES.register("Switch")
124  """Generate a Switch accessory."""
125 
126  def __init__(self, *args: Any) -> None:
127  """Initialize a Switch accessory object."""
128  super().__init__(*args, category=CATEGORY_SWITCH)
129  self._domain_domain, self._object_id_object_id = split_entity_id(self.entity_identity_id)
130  state = self.hasshass.states.get(self.entity_identity_id)
131  assert state
132 
133  self.activate_onlyactivate_only = self.is_activateis_activate(state)
134 
135  serv_switch = self.add_preload_service(SERV_SWITCH)
136  self.char_onchar_on = serv_switch.configure_char(
137  CHAR_ON, value=False, setter_callback=self.set_stateset_state
138  )
139  # Set the state so it is in sync on initial
140  # GET to avoid an event storm after homekit startup
141  self.async_update_stateasync_update_stateasync_update_state(state)
142 
143  def is_activate(self, state: State) -> bool:
144  """Check if entity is activate only."""
145  return self._domain_domain in ACTIVATE_ONLY_SWITCH_DOMAINS
146 
147  def reset_switch(self, *args: Any) -> None:
148  """Reset switch to emulate activate click."""
149  _LOGGER.debug("%s: Reset switch to off", self.entity_identity_id)
150  self.char_onchar_on.set_value(False)
151 
152  def set_state(self, value: bool) -> None:
153  """Move switch state to value if call came from HomeKit."""
154  _LOGGER.debug("%s: Set switch state to %s", self.entity_identity_id, value)
155  if self.activate_onlyactivate_only and not value:
156  _LOGGER.debug("%s: Ignoring turn_off call", self.entity_identity_id)
157  return
158 
159  params = {ATTR_ENTITY_ID: self.entity_identity_id}
160  if self._domain_domain == "script":
161  service = self._object_id_object_id
162  params = {}
163  elif self._domain_domain == button.DOMAIN:
164  service = button.SERVICE_PRESS
165  elif self._domain_domain == input_button.DOMAIN:
166  service = input_button.SERVICE_PRESS
167  else:
168  service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
169 
170  self.async_call_serviceasync_call_service(self._domain_domain, service, params)
171 
172  if self.activate_onlyactivate_only:
173  async_call_later(self.hasshass, ACTIVATE_ONLY_RESET_SECONDS, self.reset_switchreset_switch)
174 
175  @callback
176  def async_update_state(self, new_state: State) -> None:
177  """Update switch state after state changed."""
178  self.activate_onlyactivate_only = self.is_activateis_activate(new_state)
179  if self.activate_onlyactivate_only:
180  _LOGGER.debug(
181  "%s: Ignore state change, entity is activate only", self.entity_identity_id
182  )
183  return
184 
185  current_state = new_state.state == STATE_ON
186  _LOGGER.debug("%s: Set current state to %s", self.entity_identity_id, current_state)
187  self.char_onchar_on.set_value(current_state)
188 
189 
190 @TYPES.register("Vacuum")
191 class Vacuum(Switch):
192  """Generate a Switch accessory."""
193 
194  def set_state(self, value: bool) -> None:
195  """Move switch state to value if call came from HomeKit."""
196  _LOGGER.debug("%s: Set switch state to %s", self.entity_identity_id, value)
197  state = self.hasshass.states.get(self.entity_identity_id)
198  assert state
199 
200  features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
201 
202  if value:
203  sup_start = features & VacuumEntityFeature.START
204  service = SERVICE_START if sup_start else SERVICE_TURN_ON
205  else:
206  sup_return_home = features & VacuumEntityFeature.RETURN_HOME
207  service = SERVICE_RETURN_TO_BASE if sup_return_home else SERVICE_TURN_OFF
208 
209  self.async_call_serviceasync_call_service(
210  VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_identity_id}
211  )
212 
213  @callback
214  def async_update_state(self, new_state: State) -> None:
215  """Update switch state after state changed."""
216  current_state = new_state.state in (STATE_CLEANING, STATE_ON)
217  _LOGGER.debug("%s: Set current state to %s", self.entity_identity_id, current_state)
218  self.char_onchar_on.set_value(current_state)
219 
220 
222  """Valve base class."""
223 
224  def __init__(
225  self,
226  valve_type: str,
227  open_states: set[str],
228  on_service: str,
229  off_service: str,
230  *args: Any,
231  **kwargs: Any,
232  ) -> None:
233  """Initialize a Valve accessory object."""
234  super().__init__(*args, **kwargs)
235  self.domaindomain = split_entity_id(self.entity_identity_id)[0]
236  state = self.hasshass.states.get(self.entity_identity_id)
237  assert state
238 
239  self.categorycategorycategory = VALVE_TYPE[valve_type].category
240  self.open_statesopen_states = open_states
241  self.on_serviceon_service = on_service
242  self.off_serviceoff_service = off_service
243 
244  serv_valve = self.add_preload_service(SERV_VALVE)
245  self.char_activechar_active = serv_valve.configure_char(
246  CHAR_ACTIVE, value=False, setter_callback=self.set_stateset_state
247  )
248  self.char_in_usechar_in_use = serv_valve.configure_char(CHAR_IN_USE, value=False)
249  self.char_valve_typechar_valve_type = serv_valve.configure_char(
250  CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type
251  )
252  # Set the state so it is in sync on initial
253  # GET to avoid an event storm after homekit startup
254  self.async_update_stateasync_update_stateasync_update_state(state)
255 
256  def set_state(self, value: bool) -> None:
257  """Move value state to value if call came from HomeKit."""
258  _LOGGER.debug("%s: Set switch state to %s", self.entity_identity_id, value)
259  self.char_in_usechar_in_use.set_value(value)
260  params = {ATTR_ENTITY_ID: self.entity_identity_id}
261  service = self.on_serviceon_service if value else self.off_serviceoff_service
262  self.async_call_serviceasync_call_service(self.domaindomain, service, params)
263 
264  @callback
265  def async_update_state(self, new_state: State) -> None:
266  """Update switch state after state changed."""
267  current_state = 1 if new_state.state in self.open_statesopen_states else 0
268  _LOGGER.debug("%s: Set active state to %s", self.entity_identity_id, current_state)
269  self.char_activechar_active.set_value(current_state)
270  _LOGGER.debug("%s: Set in_use state to %s", self.entity_identity_id, current_state)
271  self.char_in_usechar_in_use.set_value(current_state)
272 
273 
274 @TYPES.register("ValveSwitch")
276  """Generate a Valve accessory from a HomeAssistant switch."""
277 
278  def __init__(
279  self,
280  hass: HomeAssistant,
281  driver: HomeDriver,
282  name: str,
283  entity_id: str,
284  aid: int,
285  config: dict[str, Any],
286  *args: Any,
287  ) -> None:
288  """Initialize a Valve accessory object."""
289  super().__init__(
290  config[CONF_TYPE],
291  {STATE_ON},
292  SERVICE_TURN_ON,
293  SERVICE_TURN_OFF,
294  hass,
295  driver,
296  name,
297  entity_id,
298  aid,
299  config,
300  *args,
301  )
302 
303 
304 @TYPES.register("Valve")
306  """Generate a Valve accessory from a HomeAssistant valve."""
307 
308  def __init__(self, *args: Any) -> None:
309  """Initialize a Valve accessory object."""
310  super().__init__(
311  TYPE_VALVE,
312  VALVE_OPEN_STATES,
313  SERVICE_OPEN_VALVE,
314  SERVICE_CLOSE_VALVE,
315  *args,
316  )
317 
318 
319 @TYPES.register("SelectSwitch")
321  """Generate a Switch accessory that contains multiple switches."""
322 
323  def __init__(self, *args: Any) -> None:
324  """Initialize a Switch accessory object."""
325  super().__init__(*args, category=CATEGORY_SWITCH)
326  self.domaindomain = split_entity_id(self.entity_identity_id)[0]
327  state = self.hasshass.states.get(self.entity_identity_id)
328  assert state
329 
330  self.select_chars: dict[str, Characteristic] = {}
331  options = state.attributes[ATTR_OPTIONS]
332  for option in options:
333  serv_option = self.add_preload_service(
334  SERV_OUTLET, [CHAR_NAME, CHAR_IN_USE], unique_id=option
335  )
336  serv_option.configure_char(
337  CHAR_NAME, value=cleanup_name_for_homekit(option)
338  )
339  serv_option.configure_char(CHAR_IN_USE, value=False)
340  self.select_chars[option] = serv_option.configure_char(
341  CHAR_ON,
342  value=False,
343  setter_callback=lambda value, option=option: self.select_optionselect_option(option),
344  )
345  self.set_primary_service(self.select_chars[options[0]])
346  # Set the state so it is in sync on initial
347  # GET to avoid an event storm after homekit startup
348  self.async_update_stateasync_update_stateasync_update_state(state)
349 
350  def select_option(self, option: str) -> None:
351  """Set option from HomeKit."""
352  _LOGGER.debug("%s: Set option to %s", self.entity_identity_id, option)
353  params = {ATTR_ENTITY_ID: self.entity_identity_id, "option": option}
354  self.async_call_serviceasync_call_service(self.domaindomain, SERVICE_SELECT_OPTION, params)
355 
356  @callback
357  def async_update_state(self, new_state: State) -> None:
358  """Update switch state after state changed."""
359  current_option = cleanup_name_for_homekit(new_state.state)
360  for option, char in self.select_chars.items():
361  char.set_value(option == current_option)
None async_call_service(self, str domain, str service, dict[str, Any]|None service_data, Any|None value=None)
Definition: accessories.py:609
None __init__(self, str valve_type, set[str] open_states, str on_service, str off_service, *Any args, **Any kwargs)
None __init__(self, HomeAssistant hass, HomeDriver driver, str name, str entity_id, int aid, dict[str, Any] config, *Any args)
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597