Home Assistant Unofficial Reference 2024.12.1
utils.py
Go to the documentation of this file.
1 """Shelly helpers functions."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 from datetime import datetime, timedelta
7 from ipaddress import IPv4Address, IPv6Address, ip_address
8 from types import MappingProxyType
9 from typing import Any, cast
10 
11 from aiohttp.web import Request, WebSocketResponse
12 from aioshelly.block_device import COAP, Block, BlockDevice
13 from aioshelly.const import (
14  BLOCK_GENERATIONS,
15  DEFAULT_COAP_PORT,
16  DEFAULT_HTTP_PORT,
17  MODEL_1L,
18  MODEL_DIMMER,
19  MODEL_DIMMER_2,
20  MODEL_EM3,
21  MODEL_I3,
22  MODEL_NAMES,
23  RPC_GENERATIONS,
24 )
25 from aioshelly.rpc_device import RpcDevice, WsServer
26 from yarl import URL
27 
28 from homeassistant.components import network
29 from homeassistant.components.http import HomeAssistantView
30 from homeassistant.config_entries import ConfigEntry
31 from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP
32 from homeassistant.core import Event, HomeAssistant, callback
33 from homeassistant.helpers import (
34  device_registry as dr,
35  entity_registry as er,
36  issue_registry as ir,
37  singleton,
38 )
39 from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
40 from homeassistant.helpers.network import NoURLAvailableError, get_url
41 from homeassistant.util.dt import utcnow
42 
43 from .const import (
44  API_WS_URL,
45  BASIC_INPUTS_EVENTS_TYPES,
46  COMPONENT_ID_PATTERN,
47  CONF_COAP_PORT,
48  CONF_GEN,
49  DEVICES_WITHOUT_FIRMWARE_CHANGELOG,
50  DOMAIN,
51  FIRMWARE_UNSUPPORTED_ISSUE_ID,
52  GEN1_RELEASE_URL,
53  GEN2_RELEASE_URL,
54  LOGGER,
55  RPC_INPUTS_EVENTS_TYPES,
56  SHBTN_INPUTS_EVENTS_TYPES,
57  SHBTN_MODELS,
58  SHIX3_1_INPUTS_EVENTS_TYPES,
59  UPTIME_DEVIATION,
60  VIRTUAL_COMPONENTS_MAP,
61 )
62 
63 
64 @callback
66  hass: HomeAssistant, domain: str, unique_id: str
67 ) -> None:
68  """Remove a Shelly entity."""
69  entity_reg = er.async_get(hass)
70  entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id)
71  if entity_id:
72  LOGGER.debug("Removing entity: %s", entity_id)
73  entity_reg.async_remove(entity_id)
74 
75 
76 def get_number_of_channels(device: BlockDevice, block: Block) -> int:
77  """Get number of channels for block type."""
78  channels = None
79 
80  if block.type == "input":
81  # Shelly Dimmer/1L has two input channels and missing "num_inputs"
82  if device.settings["device"]["type"] in [
83  MODEL_DIMMER,
84  MODEL_DIMMER_2,
85  MODEL_1L,
86  ]:
87  channels = 2
88  else:
89  channels = device.shelly.get("num_inputs")
90  elif block.type == "emeter":
91  channels = device.shelly.get("num_emeters")
92  elif block.type in ["relay", "light"]:
93  channels = device.shelly.get("num_outputs")
94  elif block.type in ["roller", "device"]:
95  channels = 1
96 
97  return channels or 1
98 
99 
101  device: BlockDevice,
102  block: Block | None,
103  description: str | None = None,
104 ) -> str:
105  """Naming for block based switch and sensors."""
106  channel_name = get_block_channel_name(device, block)
107 
108  if description:
109  return f"{channel_name} {description.lower()}"
110 
111  return channel_name
112 
113 
114 def get_block_channel_name(device: BlockDevice, block: Block | None) -> str:
115  """Get name based on device and channel name."""
116  entity_name = device.name
117 
118  if (
119  not block
120  or block.type == "device"
121  or get_number_of_channels(device, block) == 1
122  ):
123  return entity_name
124 
125  assert block.channel
126 
127  channel_name: str | None = None
128  mode = cast(str, block.type) + "s"
129  if mode in device.settings:
130  channel_name = device.settings[mode][int(block.channel)].get("name")
131 
132  if channel_name:
133  return channel_name
134 
135  if device.settings["device"]["type"] == MODEL_EM3:
136  base = ord("A")
137  else:
138  base = ord("1")
139 
140  return f"{entity_name} channel {chr(int(block.channel)+base)}"
141 
142 
144  settings: dict[str, Any], block: Block, include_detached: bool = False
145 ) -> bool:
146  """Return true if block input button settings is set to a momentary type."""
147  momentary_types = ["momentary", "momentary_on_release"]
148 
149  if include_detached:
150  momentary_types.append("detached")
151 
152  # Shelly Button type is fixed to momentary and no btn_type
153  if settings["device"]["type"] in SHBTN_MODELS:
154  return True
155 
156  if settings.get("mode") == "roller":
157  button_type = settings["rollers"][0]["button_type"]
158  return button_type in momentary_types
159 
160  button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
161  if button is None:
162  return False
163 
164  # Shelly 1L has two button settings in the first channel
165  if settings["device"]["type"] == MODEL_1L:
166  channel = int(block.channel or 0) + 1
167  button_type = button[0].get("btn" + str(channel) + "_type")
168  else:
169  # Some devices has only one channel in settings
170  channel = min(int(block.channel or 0), len(button) - 1)
171  button_type = button[channel].get("btn_type")
172 
173  return button_type in momentary_types
174 
175 
176 def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime:
177  """Return device uptime string, tolerate up to 5 seconds deviation."""
178  delta_uptime = utcnow() - timedelta(seconds=uptime)
179 
180  if (
181  not last_uptime
182  or abs((delta_uptime - last_uptime).total_seconds()) > UPTIME_DEVIATION
183  ):
184  return delta_uptime
185 
186  return last_uptime
187 
188 
190  device: BlockDevice, block: Block
191 ) -> list[tuple[str, str]]:
192  """Return list of input triggers for block."""
193  if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids:
194  return []
195 
196  if not is_block_momentary_input(device.settings, block, True):
197  return []
198 
199  if block.type == "device" or get_number_of_channels(device, block) == 1:
200  subtype = "button"
201  else:
202  assert block.channel
203  subtype = f"button{int(block.channel)+1}"
204 
205  if device.settings["device"]["type"] in SHBTN_MODELS:
206  trigger_types = SHBTN_INPUTS_EVENTS_TYPES
207  elif device.settings["device"]["type"] == MODEL_I3:
208  trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES
209  else:
210  trigger_types = BASIC_INPUTS_EVENTS_TYPES
211 
212  return [(trigger_type, subtype) for trigger_type in trigger_types]
213 
214 
215 def get_shbtn_input_triggers() -> list[tuple[str, str]]:
216  """Return list of input triggers for SHBTN models."""
217  return [(trigger_type, "button") for trigger_type in SHBTN_INPUTS_EVENTS_TYPES]
218 
219 
220 @singleton.singleton("shelly_coap")
221 async def get_coap_context(hass: HomeAssistant) -> COAP:
222  """Get CoAP context to be used in all Shelly Gen1 devices."""
223  context = COAP()
224 
225  adapters = await network.async_get_adapters(hass)
226  LOGGER.debug("Network adapters: %s", adapters)
227 
228  ipv4: list[IPv4Address] = []
229  if not network.async_only_default_interface_enabled(adapters):
230  ipv4.extend(
231  address
232  for address in await network.async_get_enabled_source_ips(hass)
233  if address.version == 4
234  and not (
235  address.is_link_local
236  or address.is_loopback
237  or address.is_multicast
238  or address.is_unspecified
239  )
240  )
241  LOGGER.debug("Network IPv4 addresses: %s", ipv4)
242  if DOMAIN in hass.data:
243  port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT)
244  else:
245  port = DEFAULT_COAP_PORT
246  LOGGER.info("Starting CoAP context with UDP port %s", port)
247  await context.initialize(port, ipv4)
248 
249  @callback
250  def shutdown_listener(ev: Event) -> None:
251  context.close()
252 
253  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_listener)
254  return context
255 
256 
257 class ShellyReceiver(HomeAssistantView):
258  """Handle pushes from Shelly Gen2 devices."""
259 
260  requires_auth = False
261  url = API_WS_URL
262  name = "api:shelly:ws"
263 
264  def __init__(self, ws_server: WsServer) -> None:
265  """Initialize the Shelly receiver view."""
266  self._ws_server_ws_server = ws_server
267 
268  async def get(self, request: Request) -> WebSocketResponse:
269  """Start a get request."""
270  return await self._ws_server_ws_server.websocket_handler(request)
271 
272 
273 @singleton.singleton("shelly_ws_server")
274 async def get_ws_context(hass: HomeAssistant) -> WsServer:
275  """Get websocket server context to be used in all Shelly Gen2 devices."""
276  ws_server = WsServer()
277  hass.http.register_view(ShellyReceiver(ws_server))
278  return ws_server
279 
280 
281 def get_block_device_sleep_period(settings: dict[str, Any]) -> int:
282  """Return the device sleep period in seconds or 0 for non sleeping devices."""
283  sleep_period = 0
284 
285  if settings.get("sleep_mode", False):
286  sleep_period = settings["sleep_mode"]["period"]
287  if settings["sleep_mode"]["unit"] == "h":
288  sleep_period *= 60 # hours to minutes
289 
290  return sleep_period * 60 # minutes to seconds
291 
292 
293 def get_rpc_device_wakeup_period(status: dict[str, Any]) -> int:
294  """Return the device wakeup period in seconds or 0 for non sleeping devices."""
295  return cast(int, status["sys"].get("wakeup_period", 0))
296 
297 
298 def get_info_auth(info: dict[str, Any]) -> bool:
299  """Return true if device has authorization enabled."""
300  return cast(bool, info.get("auth") or info.get("auth_en"))
301 
302 
303 def get_info_gen(info: dict[str, Any]) -> int:
304  """Return the device generation from shelly info."""
305  return int(info.get(CONF_GEN, 1))
306 
307 
308 def get_model_name(info: dict[str, Any]) -> str:
309  """Return the device model name."""
310  if get_info_gen(info) in RPC_GENERATIONS:
311  return cast(str, MODEL_NAMES.get(info["model"], info["model"]))
312 
313  return cast(str, MODEL_NAMES.get(info["type"], info["type"]))
314 
315 
316 def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
317  """Get name based on device and channel name."""
318  key = key.replace("emdata", "em")
319  key = key.replace("em1data", "em1")
320  device_name = device.name
321  entity_name: str | None = None
322  if key in device.config:
323  entity_name = device.config[key].get("name")
324 
325  if entity_name is None:
326  channel = key.split(":")[0]
327  channel_id = key.split(":")[-1]
328  if key.startswith(("cover:", "input:", "light:", "switch:", "thermostat:")):
329  return f"{device_name} {channel.title()} {channel_id}"
330  if key.startswith(("cct", "rgb:", "rgbw:")):
331  return f"{device_name} {channel.upper()} light {channel_id}"
332  if key.startswith("em1"):
333  return f"{device_name} EM{channel_id}"
334  if key.startswith(("boolean:", "enum:", "number:", "text:")):
335  return f"{channel.title()} {channel_id}"
336  return device_name
337 
338  return entity_name
339 
340 
342  device: RpcDevice, key: str, description: str | None = None
343 ) -> str:
344  """Naming for RPC based switch and sensors."""
345  channel_name = get_rpc_channel_name(device, key)
346 
347  if description:
348  return f"{channel_name} {description.lower()}"
349 
350  return channel_name
351 
352 
353 def get_device_entry_gen(entry: ConfigEntry) -> int:
354  """Return the device generation from config entry."""
355  return entry.data.get(CONF_GEN, 1)
356 
357 
358 def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]:
359  """Return list of key instances for RPC device from a dict."""
360  if key in keys_dict:
361  return [key]
362 
363  if key == "switch" and "cover:0" in keys_dict:
364  key = "cover"
365 
366  return [k for k in keys_dict if k.startswith(f"{key}:")]
367 
368 
369 def get_rpc_key_ids(keys_dict: dict[str, Any], key: str) -> list[int]:
370  """Return list of key ids for RPC device from a dict."""
371  return [int(k.split(":")[1]) for k in keys_dict if k.startswith(f"{key}:")]
372 
373 
375  config: dict[str, Any], status: dict[str, Any], key: str
376 ) -> bool:
377  """Return true if rpc input button settings is set to a momentary type."""
378  return cast(bool, config[key]["type"] == "button")
379 
380 
381 def is_block_channel_type_light(settings: dict[str, Any], channel: int) -> bool:
382  """Return true if block channel appliance type is set to light."""
383  app_type = settings["relays"][channel].get("appliance_type")
384  return app_type is not None and app_type.lower().startswith("light")
385 
386 
387 def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
388  """Return true if rpc channel consumption type is set to light."""
389  con_types = config["sys"].get("ui_data", {}).get("consumption_types")
390  if con_types is None or len(con_types) <= channel:
391  return False
392  return cast(str, con_types[channel]).lower().startswith("light")
393 
394 
395 def is_rpc_thermostat_internal_actuator(status: dict[str, Any]) -> bool:
396  """Return true if the thermostat uses an internal relay."""
397  return cast(bool, status["sys"].get("relay_in_thermostat", False))
398 
399 
400 def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
401  """Return list of input triggers for RPC device."""
402  triggers = []
403 
404  key_ids = get_rpc_key_ids(device.config, "input")
405 
406  for id_ in key_ids:
407  key = f"input:{id_}"
408  if not is_rpc_momentary_input(device.config, device.status, key):
409  continue
410 
411  for trigger_type in RPC_INPUTS_EVENTS_TYPES:
412  subtype = f"button{id_+1}"
413  triggers.append((trigger_type, subtype))
414 
415  return triggers
416 
417 
418 @callback
420  hass: HomeAssistant, shellydevice: BlockDevice | RpcDevice, entry: ConfigEntry
421 ) -> None:
422  """Update the firmware version information in the device registry."""
423  assert entry.unique_id
424 
425  dev_reg = dr.async_get(hass)
426  if device := dev_reg.async_get_device(
427  identifiers={(DOMAIN, entry.entry_id)},
428  connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))},
429  ):
430  if device.sw_version == shellydevice.firmware_version:
431  return
432 
433  LOGGER.debug("Updating device registry info for %s", entry.title)
434 
435  dev_reg.async_update_device(device.id, sw_version=shellydevice.firmware_version)
436 
437 
438 def brightness_to_percentage(brightness: int) -> int:
439  """Convert brightness level to percentage."""
440  return int(100 * (brightness + 1) / 255)
441 
442 
443 def percentage_to_brightness(percentage: int) -> int:
444  """Convert percentage to brightness level."""
445  return round(255 * percentage / 100)
446 
447 
448 def mac_address_from_name(name: str) -> str | None:
449  """Convert a name to a mac address."""
450  mac = name.partition(".")[0].partition("-")[-1]
451  return mac.upper() if len(mac) == 12 else None
452 
453 
454 def get_release_url(gen: int, model: str, beta: bool) -> str | None:
455  """Return release URL or None."""
456  if beta or model in DEVICES_WITHOUT_FIRMWARE_CHANGELOG:
457  return None
458 
459  return GEN1_RELEASE_URL if gen in BLOCK_GENERATIONS else GEN2_RELEASE_URL
460 
461 
462 @callback
464  hass: HomeAssistant, entry: ConfigEntry
465 ) -> None:
466  """Create a repair issue if the device runs an unsupported firmware."""
467  ir.async_create_issue(
468  hass,
469  DOMAIN,
470  FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=entry.unique_id),
471  is_fixable=False,
472  is_persistent=False,
473  severity=ir.IssueSeverity.ERROR,
474  translation_key="unsupported_firmware",
475  translation_placeholders={
476  "device_name": entry.title,
477  "ip_address": entry.data["host"],
478  },
479  )
480 
481 
483  config: dict[str, Any], _status: dict[str, Any], key: str
484 ) -> bool:
485  """Return true if rpc all WiFi stations are disabled."""
486  if config[key]["sta"]["enable"] is True or config[key]["sta1"]["enable"] is True:
487  return False
488 
489  return True
490 
491 
492 def get_http_port(data: MappingProxyType[str, Any]) -> int:
493  """Get port from config entry data."""
494  return cast(int, data.get(CONF_PORT, DEFAULT_HTTP_PORT))
495 
496 
497 def get_host(host: str) -> str:
498  """Get the device IP address or hostname."""
499  try:
500  ip_object = ip_address(host)
501  except ValueError:
502  # host contains hostname
503  return host
504 
505  if isinstance(ip_object, IPv6Address):
506  return f"[{host}]"
507 
508  return host
509 
510 
511 @callback
513  hass: HomeAssistant, domain: str, mac: str, keys: list[str]
514 ) -> None:
515  """Remove RPC based Shelly entity."""
516  entity_reg = er.async_get(hass)
517  for key in keys:
518  if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"):
519  LOGGER.debug("Removing entity: %s", entity_id)
520  entity_reg.async_remove(entity_id)
521 
522 
523 def is_rpc_thermostat_mode(ident: int, status: dict[str, Any]) -> bool:
524  """Return True if 'thermostat:<IDent>' is present in the status."""
525  return f"thermostat:{ident}" in status
526 
527 
528 def get_virtual_component_ids(config: dict[str, Any], platform: str) -> list[str]:
529  """Return a list of virtual component IDs for a platform."""
530  component = VIRTUAL_COMPONENTS_MAP.get(platform)
531 
532  if not component:
533  return []
534 
535  ids: list[str] = []
536 
537  for comp_type in component["types"]:
538  ids.extend(
539  k
540  for k, v in config.items()
541  if k.startswith(comp_type) and v["meta"]["ui"]["view"] in component["modes"]
542  )
543 
544  return ids
545 
546 
547 @callback
549  hass: HomeAssistant,
550  config_entry_id: str,
551  mac: str,
552  platform: str,
553  keys: Iterable[str],
554  key_suffix: str | None = None,
555 ) -> None:
556  """Remove orphaned entities."""
557  orphaned_entities = []
558  entity_reg = er.async_get(hass)
559  device_reg = dr.async_get(hass)
560 
561  if not (
562  devices := device_reg.devices.get_devices_for_config_entry_id(config_entry_id)
563  ):
564  return
565 
566  device_id = devices[0].id
567  entities = er.async_entries_for_device(entity_reg, device_id, True)
568  for entity in entities:
569  if not entity.entity_id.startswith(platform):
570  continue
571  if key_suffix is not None and key_suffix not in entity.unique_id:
572  continue
573  # we are looking for the component ID, e.g. boolean:201, em1data:1
574  if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
575  continue
576 
577  key = match.group()
578  if key not in keys:
579  orphaned_entities.append(entity.unique_id.split("-", 1)[1])
580 
581  if orphaned_entities:
582  async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)
583 
584 
585 def get_rpc_ws_url(hass: HomeAssistant) -> str | None:
586  """Return the RPC websocket URL."""
587  try:
588  raw_url = get_url(hass, prefer_external=False, allow_cloud=False)
589  except NoURLAvailableError:
590  LOGGER.debug("URL not available, skipping outbound websocket setup")
591  return None
592  url = URL(raw_url)
593  ws_url = url.with_scheme("wss" if url.scheme == "https" else "ws")
594  return str(ws_url.joinpath(API_WS_URL.removeprefix("/")))
None __init__(self, WsServer ws_server)
Definition: utils.py:264
WebSocketResponse get(self, Request request)
Definition: utils.py:268
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool get_info_auth(dict[str, Any] info)
Definition: utils.py:298
bool is_rpc_channel_type_light(dict[str, Any] config, int channel)
Definition: utils.py:387
bool is_rpc_momentary_input(dict[str, Any] config, dict[str, Any] status, str key)
Definition: utils.py:376
list[tuple[str, str]] get_shbtn_input_triggers()
Definition: utils.py:215
bool is_rpc_thermostat_internal_actuator(dict[str, Any] status)
Definition: utils.py:395
int percentage_to_brightness(int percentage)
Definition: utils.py:443
list[tuple[str, str]] get_rpc_input_triggers(RpcDevice device)
Definition: utils.py:400
WsServer get_ws_context(HomeAssistant hass)
Definition: utils.py:274
list[str] get_virtual_component_ids(dict[str, Any] config, str platform)
Definition: utils.py:528
None async_remove_shelly_rpc_entities(HomeAssistant hass, str domain, str mac, list[str] keys)
Definition: utils.py:514
datetime get_device_uptime(float uptime, datetime|None last_uptime)
Definition: utils.py:176
list[tuple[str, str]] get_block_input_triggers(BlockDevice device, Block block)
Definition: utils.py:191
list[str] get_rpc_key_instances(dict[str, Any] keys_dict, str key)
Definition: utils.py:358
int get_info_gen(dict[str, Any] info)
Definition: utils.py:303
bool is_block_momentary_input(dict[str, Any] settings, Block block, bool include_detached=False)
Definition: utils.py:145
str get_rpc_channel_name(RpcDevice device, str key)
Definition: utils.py:316
bool is_rpc_wifi_stations_disabled(dict[str, Any] config, dict[str, Any] _status, str key)
Definition: utils.py:484
bool is_rpc_thermostat_mode(int ident, dict[str, Any] status)
Definition: utils.py:523
None async_remove_orphaned_entities(HomeAssistant hass, str config_entry_id, str mac, str platform, Iterable[str] keys, str|None key_suffix=None)
Definition: utils.py:555
str get_block_entity_name(BlockDevice device, Block|None block, str|None description=None)
Definition: utils.py:104
str get_model_name(dict[str, Any] info)
Definition: utils.py:308
COAP get_coap_context(HomeAssistant hass)
Definition: utils.py:221
int get_rpc_device_wakeup_period(dict[str, Any] status)
Definition: utils.py:293
str|None get_rpc_ws_url(HomeAssistant hass)
Definition: utils.py:585
None async_create_issue_unsupported_firmware(HomeAssistant hass, ConfigEntry entry)
Definition: utils.py:465
None update_device_fw_info(HomeAssistant hass, BlockDevice|RpcDevice shellydevice, ConfigEntry entry)
Definition: utils.py:421
int brightness_to_percentage(int brightness)
Definition: utils.py:438
list[int] get_rpc_key_ids(dict[str, Any] keys_dict, str key)
Definition: utils.py:369
str|None mac_address_from_name(str name)
Definition: utils.py:448
int get_device_entry_gen(ConfigEntry entry)
Definition: utils.py:353
str|None get_release_url(int gen, str model, bool beta)
Definition: utils.py:454
int get_http_port(MappingProxyType[str, Any] data)
Definition: utils.py:492
bool is_block_channel_type_light(dict[str, Any] settings, int channel)
Definition: utils.py:381
None async_remove_shelly_entity(HomeAssistant hass, str domain, str unique_id)
Definition: utils.py:67
str get_block_channel_name(BlockDevice device, Block|None block)
Definition: utils.py:114
int get_block_device_sleep_period(dict[str, Any] settings)
Definition: utils.py:281
str get_rpc_entity_name(RpcDevice device, str key, str|None description=None)
Definition: utils.py:343
int get_number_of_channels(BlockDevice device, Block block)
Definition: utils.py:76
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
Definition: network.py:131