Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Support for Rflink devices."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 
8 from rflink.protocol import ProtocolBase
9 
10 from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, STATE_ON
11 from homeassistant.core import callback
12 from homeassistant.exceptions import HomeAssistantError
13 from homeassistant.helpers.dispatcher import async_dispatcher_connect
14 from homeassistant.helpers.entity import Entity
15 from homeassistant.helpers.restore_state import RestoreEntity
16 
17 from .const import (
18  DATA_ENTITY_GROUP_LOOKUP,
19  DATA_ENTITY_LOOKUP,
20  DEFAULT_SIGNAL_REPETITIONS,
21  EVENT_KEY_COMMAND,
22  SIGNAL_AVAILABILITY,
23  SIGNAL_HANDLE_EVENT,
24  TMP_ENTITY,
25 )
26 from .utils import brightness_to_rflink, identify_event_type
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 EVENT_BUTTON_PRESSED = "button_pressed"
31 
32 
34  """Representation of a Rflink device.
35 
36  Contains the common logic for Rflink entities.
37  """
38 
39  _state: bool | None = None
40  _available = True
41  _attr_should_poll = False
42 
43  def __init__(
44  self,
45  device_id,
46  initial_event=None,
47  name=None,
48  aliases=None,
49  group=True,
50  group_aliases=None,
51  nogroup_aliases=None,
52  fire_event=False,
53  signal_repetitions=DEFAULT_SIGNAL_REPETITIONS,
54  ):
55  """Initialize the device."""
56  # Rflink specific attributes for every component type
57  self._initial_event_initial_event = initial_event
58  self._device_id_device_id = device_id
59  self._attr_unique_id_attr_unique_id = device_id
60  if name:
61  self._name_name = name
62  else:
63  self._name_name = device_id
64 
65  self._aliases_aliases = aliases
66  self._group_group = group
67  self._group_aliases_group_aliases = group_aliases
68  self._nogroup_aliases_nogroup_aliases = nogroup_aliases
69  self._should_fire_event_should_fire_event = fire_event
70  self._signal_repetitions_signal_repetitions = signal_repetitions
71 
72  @callback
73  def handle_event_callback(self, event):
74  """Handle incoming event for device type."""
75  # Call platform specific event handler
76  self._handle_event_handle_event(event)
77 
78  # Propagate changes through ha
79  self.async_write_ha_stateasync_write_ha_state()
80 
81  # Put command onto bus for user to subscribe to
82  if self._should_fire_event_should_fire_event and identify_event_type(event) == EVENT_KEY_COMMAND:
83  self.hasshass.bus.async_fire(
84  EVENT_BUTTON_PRESSED,
85  {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_STATE: event[EVENT_KEY_COMMAND]},
86  )
87  _LOGGER.debug(
88  "Fired bus event for %s: %s", self.entity_identity_id, event[EVENT_KEY_COMMAND]
89  )
90 
91  def _handle_event(self, event):
92  """Platform specific event handler."""
93  raise NotImplementedError
94 
95  @property
96  def name(self):
97  """Return a name for the device."""
98  return self._name_name
99 
100  @property
101  def is_on(self):
102  """Return true if device is on."""
103  if self.assumed_stateassumed_stateassumed_state:
104  return False
105  return self._state
106 
107  @property
108  def assumed_state(self):
109  """Assume device state until first device event sets state."""
110  return self._state is None
111 
112  @property
113  def available(self):
114  """Return True if entity is available."""
115  return self._available_available_available
116 
117  @callback
118  def _availability_callback(self, availability):
119  """Update availability state."""
120  self._available_available_available = availability
121  self.async_write_ha_stateasync_write_ha_state()
122 
123  async def async_added_to_hass(self):
124  """Register update callback."""
125  await super().async_added_to_hass()
126  # Remove temporary bogus entity_id if added
127  tmp_entity = TMP_ENTITY.format(self._device_id_device_id)
128  if (
129  tmp_entity
130  in self.hasshass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][self._device_id_device_id]
131  ):
132  self.hasshass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][
133  self._device_id_device_id
134  ].remove(tmp_entity)
135 
136  # Register id and aliases
137  self.hasshass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][self._device_id_device_id].append(
138  self.entity_identity_id
139  )
140  if self._group_group:
141  self.hasshass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND][
142  self._device_id_device_id
143  ].append(self.entity_identity_id)
144  # aliases respond to both normal and group commands (allon/alloff)
145  if self._aliases_aliases:
146  for _id in self._aliases_aliases:
147  self.hasshass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][_id].append(
148  self.entity_identity_id
149  )
150  self.hasshass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND][_id].append(
151  self.entity_identity_id
152  )
153  # group_aliases only respond to group commands (allon/alloff)
154  if self._group_aliases_group_aliases:
155  for _id in self._group_aliases_group_aliases:
156  self.hasshass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND][_id].append(
157  self.entity_identity_id
158  )
159  # nogroup_aliases only respond to normal commands
160  if self._nogroup_aliases_nogroup_aliases:
161  for _id in self._nogroup_aliases_nogroup_aliases:
162  self.hasshass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][_id].append(
163  self.entity_identity_id
164  )
165  self.async_on_removeasync_on_remove(
167  self.hasshass, SIGNAL_AVAILABILITY, self._availability_callback_availability_callback
168  )
169  )
170  self.async_on_removeasync_on_remove(
172  self.hasshass,
173  SIGNAL_HANDLE_EVENT.format(self.entity_identity_id),
174  self.handle_event_callbackhandle_event_callback,
175  )
176  )
177 
178  # Process the initial event now that the entity is created
179  if self._initial_event_initial_event:
180  self.handle_event_callbackhandle_event_callback(self._initial_event_initial_event)
181 
182 
184  """Singleton class to make Rflink command interface available to entities.
185 
186  This class is to be inherited by every Entity class that is actionable
187  (switches/lights). It exposes the Rflink command interface for these
188  entities.
189 
190  The Rflink interface is managed as a class level and set during setup (and
191  reset on reconnect).
192  """
193 
194  # Keep repetition tasks to cancel if state is changed before repetitions
195  # are sent
196  _repetition_task: asyncio.Task[None] | None = None
197 
198  _protocol: ProtocolBase | None = None
199 
200  _wait_ack: bool | None = None
201 
202  @classmethod
204  cls, protocol: ProtocolBase | None, wait_ack: bool | None = None
205  ) -> None:
206  """Set the Rflink asyncio protocol as a class variable."""
207  cls._protocol_protocol = protocol
208  if wait_ack is not None:
209  cls._wait_ack_wait_ack = wait_ack
210 
211  @classmethod
212  def is_connected(cls):
213  """Return connection status."""
214  return bool(cls._protocol_protocol)
215 
216  @classmethod
217  async def send_command(cls, device_id, action):
218  """Send device command to Rflink and wait for acknowledgement."""
219  return await cls._protocol_protocol.send_command_ack(device_id, action)
220 
221  async def _async_handle_command(self, command, *args):
222  """Do bookkeeping for command, send it to rflink and update state."""
223  self.cancel_queued_send_commandscancel_queued_send_commands()
224 
225  if command == "turn_on":
226  cmd = "on"
227  self._state_state = True
228 
229  elif command == "turn_off":
230  cmd = "off"
231  self._state_state = False
232 
233  elif command == "dim":
234  # convert brightness to rflink dim level
235  cmd = str(brightness_to_rflink(args[0]))
236  self._state_state = True
237 
238  elif command == "toggle":
239  cmd = "on"
240  # if the state is unknown or false, it gets set as true
241  # if the state is true, it gets set as false
242  self._state_state = self._state_state in [None, False]
243 
244  # Cover options for RFlink
245  elif command == "close_cover":
246  cmd = "DOWN"
247  self._state_state = False
248 
249  elif command == "open_cover":
250  cmd = "UP"
251  self._state_state = True
252 
253  elif command == "stop_cover":
254  cmd = "STOP"
255  self._state_state = True
256 
257  # Send initial command and queue repetitions.
258  # This allows the entity state to be updated quickly and not having to
259  # wait for all repetitions to be sent
260  await self._async_send_command_async_send_command(cmd, self._signal_repetitions_signal_repetitions)
261 
262  # Update state of entity
263  self.async_write_ha_stateasync_write_ha_state()
264 
266  """Cancel queued signal repetition commands.
267 
268  For example when user changed state while repetitions are still
269  queued for broadcast. Or when an incoming Rflink command (remote
270  switch) changes the state.
271  """
272  # cancel any outstanding tasks from the previous state change
273  if self._repetition_task_repetition_task:
274  self._repetition_task_repetition_task.cancel()
275 
276  async def _async_send_command(self, cmd, repetitions):
277  """Send a command for device to Rflink gateway."""
278  _LOGGER.debug("Sending command: %s to Rflink device: %s", cmd, self._device_id_device_id)
279 
280  if not self.is_connectedis_connected():
281  raise HomeAssistantError("Cannot send command, not connected!")
282 
283  if self._wait_ack_wait_ack:
284  # Puts command on outgoing buffer then waits for Rflink to confirm
285  # the command has been sent out.
286  await self._protocol_protocol.send_command_ack(self._device_id_device_id, cmd)
287  else:
288  # Puts command on outgoing buffer and returns straight away.
289  # Rflink protocol/transport handles asynchronous writing of buffer
290  # to serial/tcp device. Does not wait for command send
291  # confirmation.
292  self._protocol_protocol.send_command(self._device_id_device_id, cmd)
293 
294  if repetitions > 1:
295  self._repetition_task_repetition_task = self.hasshass.async_create_task(
296  self._async_send_command_async_send_command(cmd, repetitions - 1), eager_start=False
297  )
298 
299 
301  """Rflink entity which can switch on/off (eg: light, switch)."""
302 
303  async def async_added_to_hass(self):
304  """Restore RFLink device state (ON/OFF)."""
305  await super().async_added_to_hass()
306  if (old_state := await self.async_get_last_stateasync_get_last_state()) is not None:
307  self._state_state_state = old_state.state == STATE_ON
308 
309  def _handle_event(self, event):
310  """Adjust state if Rflink picks up a remote command for this device."""
311  self.cancel_queued_send_commandscancel_queued_send_commands()
312 
313  command = event["command"]
314  if command in ["on", "allon"]:
315  self._state_state_state = True
316  elif command in ["off", "alloff"]:
317  self._state_state_state = False
318 
319  async def async_turn_on(self, **kwargs):
320  """Turn the device on."""
321  await self._async_handle_command_async_handle_command("turn_on")
322 
323  async def async_turn_off(self, **kwargs):
324  """Turn the device off."""
325  await self._async_handle_command_async_handle_command("turn_off")
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
bool remove(self, _T matcher)
Definition: match.py:214
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103