Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Xiaomi Gateways."""
2 
3 import asyncio
4 import logging
5 
6 import voluptuous as vol
7 from xiaomi_gateway import AsyncXiaomiGatewayMulticast, XiaomiGateway
8 
9 from homeassistant.components import persistent_notification
10 from homeassistant.config_entries import ConfigEntry, ConfigEntryState
11 from homeassistant.const import (
12  ATTR_DEVICE_ID,
13  CONF_HOST,
14  CONF_PORT,
15  CONF_PROTOCOL,
16  EVENT_HOMEASSISTANT_STOP,
17  Platform,
18 )
19 from homeassistant.core import HomeAssistant, ServiceCall, callback
20 from homeassistant.helpers import device_registry as dr
22 from homeassistant.helpers.typing import ConfigType
23 
24 from .const import (
25  CONF_INTERFACE,
26  CONF_KEY,
27  CONF_SID,
28  DEFAULT_DISCOVERY_RETRY,
29  DOMAIN,
30  GATEWAYS_KEY,
31  KEY_SETUP_LOCK,
32  KEY_UNSUB_STOP,
33  LISTENER_KEY,
34 )
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 GATEWAY_PLATFORMS = [
39  Platform.BINARY_SENSOR,
40  Platform.COVER,
41  Platform.LIGHT,
42  Platform.LOCK,
43  Platform.SENSOR,
44  Platform.SWITCH,
45 ]
46 GATEWAY_PLATFORMS_NO_KEY = [Platform.BINARY_SENSOR, Platform.SENSOR]
47 
48 ATTR_GW_MAC = "gw_mac"
49 ATTR_RINGTONE_ID = "ringtone_id"
50 ATTR_RINGTONE_VOL = "ringtone_vol"
51 
52 SERVICE_PLAY_RINGTONE = "play_ringtone"
53 SERVICE_STOP_RINGTONE = "stop_ringtone"
54 SERVICE_ADD_DEVICE = "add_device"
55 SERVICE_REMOVE_DEVICE = "remove_device"
56 
57 SERVICE_SCHEMA_PLAY_RINGTONE = vol.Schema(
58  {
59  vol.Required(ATTR_RINGTONE_ID): vol.All(
60  vol.Coerce(int), vol.NotIn([9, 14, 15, 16, 17, 18, 19])
61  ),
62  vol.Optional(ATTR_RINGTONE_VOL): vol.All(
63  vol.Coerce(int), vol.Clamp(min=0, max=100)
64  ),
65  }
66 )
67 
68 SERVICE_SCHEMA_REMOVE_DEVICE = vol.Schema(
69  {vol.Required(ATTR_DEVICE_ID): vol.All(cv.string, vol.Length(min=14, max=14))}
70 )
71 
72 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
73 
74 
75 def setup(hass: HomeAssistant, config: ConfigType) -> bool:
76  """Set up the Xiaomi component."""
77 
78  def play_ringtone_service(call: ServiceCall) -> None:
79  """Service to play ringtone through Gateway."""
80  ring_id = call.data.get(ATTR_RINGTONE_ID)
81  gateway: XiaomiGateway = call.data[ATTR_GW_MAC]
82 
83  kwargs = {"mid": ring_id}
84 
85  if (ring_vol := call.data.get(ATTR_RINGTONE_VOL)) is not None:
86  kwargs["vol"] = ring_vol
87 
88  gateway.write_to_hub(gateway.sid, **kwargs)
89 
90  def stop_ringtone_service(call: ServiceCall) -> None:
91  """Service to stop playing ringtone on Gateway."""
92  gateway: XiaomiGateway = call.data[ATTR_GW_MAC]
93  gateway.write_to_hub(gateway.sid, mid=10000)
94 
95  def add_device_service(call: ServiceCall) -> None:
96  """Service to add a new sub-device within the next 30 seconds."""
97  gateway: XiaomiGateway = call.data[ATTR_GW_MAC]
98  gateway.write_to_hub(gateway.sid, join_permission="yes")
99  persistent_notification.async_create(
100  hass,
101  (
102  "Join permission enabled for 30 seconds! "
103  "Please press the pairing button of the new device once."
104  ),
105  title="Xiaomi Aqara Gateway",
106  )
107 
108  def remove_device_service(call: ServiceCall) -> None:
109  """Service to remove a sub-device from the gateway."""
110  device_id = call.data.get(ATTR_DEVICE_ID)
111  gateway: XiaomiGateway = call.data[ATTR_GW_MAC]
112  gateway.write_to_hub(gateway.sid, remove_device=device_id)
113 
114  gateway_only_schema = _add_gateway_to_schema(hass, vol.Schema({}))
115 
116  hass.services.register(
117  DOMAIN,
118  SERVICE_PLAY_RINGTONE,
119  play_ringtone_service,
120  schema=_add_gateway_to_schema(hass, SERVICE_SCHEMA_PLAY_RINGTONE),
121  )
122 
123  hass.services.register(
124  DOMAIN, SERVICE_STOP_RINGTONE, stop_ringtone_service, schema=gateway_only_schema
125  )
126 
127  hass.services.register(
128  DOMAIN, SERVICE_ADD_DEVICE, add_device_service, schema=gateway_only_schema
129  )
130 
131  hass.services.register(
132  DOMAIN,
133  SERVICE_REMOVE_DEVICE,
134  remove_device_service,
135  schema=_add_gateway_to_schema(hass, SERVICE_SCHEMA_REMOVE_DEVICE),
136  )
137 
138  return True
139 
140 
141 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
142  """Set up the xiaomi aqara components from a config entry."""
143  hass.data.setdefault(DOMAIN, {})
144  setup_lock = hass.data[DOMAIN].setdefault(KEY_SETUP_LOCK, asyncio.Lock())
145  hass.data[DOMAIN].setdefault(GATEWAYS_KEY, {})
146 
147  # Connect to Xiaomi Aqara Gateway
148  xiaomi_gateway = await hass.async_add_executor_job(
149  XiaomiGateway,
150  entry.data[CONF_HOST],
151  entry.data[CONF_SID],
152  entry.data[CONF_KEY],
153  DEFAULT_DISCOVERY_RETRY,
154  entry.data[CONF_INTERFACE],
155  entry.data[CONF_PORT],
156  entry.data[CONF_PROTOCOL],
157  )
158  hass.data[DOMAIN][GATEWAYS_KEY][entry.entry_id] = xiaomi_gateway
159 
160  async with setup_lock:
161  if LISTENER_KEY not in hass.data[DOMAIN]:
162  multicast = AsyncXiaomiGatewayMulticast(
163  interface=entry.data[CONF_INTERFACE]
164  )
165  hass.data[DOMAIN][LISTENER_KEY] = multicast
166 
167  # start listining for local pushes (only once)
168  await multicast.start_listen()
169 
170  # register stop callback to shutdown listining for local pushes
171  @callback
172  def stop_xiaomi(event):
173  """Stop Xiaomi Socket."""
174  _LOGGER.debug("Shutting down Xiaomi Gateway Listener")
175  multicast.stop_listen()
176 
177  unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_xiaomi)
178  hass.data[DOMAIN][KEY_UNSUB_STOP] = unsub
179 
180  multicast = hass.data[DOMAIN][LISTENER_KEY]
181  multicast.register_gateway(entry.data[CONF_HOST], xiaomi_gateway.multicast_callback)
182  _LOGGER.debug(
183  "Gateway with host '%s' connected, listening for broadcasts",
184  entry.data[CONF_HOST],
185  )
186 
187  assert entry.unique_id
188  device_registry = dr.async_get(hass)
189  device_registry.async_get_or_create(
190  config_entry_id=entry.entry_id,
191  identifiers={(DOMAIN, entry.unique_id)},
192  manufacturer="Xiaomi Aqara",
193  name=entry.title,
194  sw_version=entry.data[CONF_PROTOCOL],
195  )
196 
197  if entry.data[CONF_KEY] is not None:
198  platforms = GATEWAY_PLATFORMS
199  else:
200  platforms = GATEWAY_PLATFORMS_NO_KEY
201 
202  await hass.config_entries.async_forward_entry_setups(entry, platforms)
203 
204  return True
205 
206 
207 async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
208  """Unload a config entry."""
209  if config_entry.data[CONF_KEY] is not None:
210  platforms = GATEWAY_PLATFORMS
211  else:
212  platforms = GATEWAY_PLATFORMS_NO_KEY
213 
214  unload_ok = await hass.config_entries.async_unload_platforms(
215  config_entry, platforms
216  )
217  if unload_ok:
218  hass.data[DOMAIN][GATEWAYS_KEY].pop(config_entry.entry_id)
219 
220  loaded_entries = [
221  entry
222  for entry in hass.config_entries.async_entries(DOMAIN)
223  if entry.state == ConfigEntryState.LOADED
224  ]
225  if len(loaded_entries) == 1:
226  # No gateways left, stop Xiaomi socket
227  unsub_stop = hass.data[DOMAIN].pop(KEY_UNSUB_STOP)
228  unsub_stop()
229  hass.data[DOMAIN].pop(GATEWAYS_KEY)
230  _LOGGER.debug("Shutting down Xiaomi Gateway Listener")
231  multicast = hass.data[DOMAIN].pop(LISTENER_KEY)
232  multicast.stop_listen()
233 
234  return unload_ok
235 
236 
237 def _add_gateway_to_schema(hass, schema):
238  """Extend a voluptuous schema with a gateway validator."""
239 
240  def gateway(sid):
241  """Convert sid to a gateway."""
242  sid = str(sid).replace(":", "").lower()
243 
244  for gateway in hass.data[DOMAIN][GATEWAYS_KEY].values():
245  if gateway.sid == sid:
246  return gateway
247 
248  raise vol.Invalid(f"Unknown gateway sid {sid}")
249 
250  kwargs = {}
251  if (xiaomi_data := hass.data.get(DOMAIN)) is not None:
252  gateways = list(xiaomi_data[GATEWAYS_KEY].values())
253 
254  # If the user has only 1 gateway, make it the default for services.
255  if len(gateways) == 1:
256  kwargs["default"] = gateways[0].sid
257 
258  return schema.extend({vol.Required(ATTR_GW_MAC, **kwargs): gateway})
def _add_gateway_to_schema(hass, schema)
Definition: __init__.py:237
bool async_unload_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:207
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:141
bool setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:75