1 """Support for WeMo device discovery."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Coroutine, Sequence
6 from datetime
import datetime
11 import voluptuous
as vol
13 from homeassistant
import config_entries
22 from .const
import DOMAIN
23 from .coordinator
import DeviceCoordinator, async_register_device
24 from .models
import WemoConfigEntryData, WemoData, async_wemo_data
31 WEMO_MODEL_DISPATCH = {
32 "Bridge": [Platform.LIGHT],
33 "CoffeeMaker": [Platform.SWITCH],
34 "Dimmer": [Platform.LIGHT],
35 "Humidifier": [Platform.FAN],
36 "Insight": [Platform.BINARY_SENSOR, Platform.SWITCH],
37 "LightSwitch": [Platform.SWITCH],
38 "Maker": [Platform.BINARY_SENSOR, Platform.SWITCH],
39 "Motion": [Platform.BINARY_SENSOR],
40 "OutdoorPlug": [Platform.SWITCH],
41 "Sensor": [Platform.BINARY_SENSOR],
42 "Socket": [Platform.SWITCH],
45 _LOGGER = logging.getLogger(__name__)
47 type DispatchCallback = Callable[[DeviceCoordinator], Coroutine[Any, Any,
None]]
48 type HostPortTuple = tuple[str, int |
None]
52 """Validate that provided value is either just host or host:port.
54 Returns (host, None) or (host, port) respectively.
56 host, _, port_str = value.partition(
":")
59 raise vol.Invalid(
"host cannot be empty")
61 port = cv.port(port_str)
if port_str
else None
66 CONF_STATIC =
"static"
68 DEFAULT_DISCOVERY =
True
70 CONFIG_SCHEMA = vol.Schema(
74 vol.Optional(CONF_STATIC, default=[]): vol.Schema(
75 [vol.All(cv.string, coerce_host_port)]
77 vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
81 extra=vol.ALLOW_EXTRA,
85 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
86 """Set up for WeMo devices."""
88 registry = pywemo.SubscriptionRegistry()
89 await hass.async_add_executor_job(registry.start)
92 discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port)
93 await hass.async_add_executor_job(discovery_responder.start)
95 def _on_hass_stop(_: Event) ->
None:
96 discovery_responder.stop()
99 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop)
101 yaml_config = config.get(DOMAIN, {})
103 discovery_enabled=yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY),
104 static_config=yaml_config.get(CONF_STATIC, []),
109 hass.async_create_task(
110 hass.config_entries.flow.async_init(
111 DOMAIN, context={
"source": config_entries.SOURCE_IMPORT}
119 """Set up a wemo config entry."""
122 discovery =
WemoDiscovery(hass, dispatcher, wemo_data.static_config, entry)
124 device_coordinators={},
126 dispatcher=dispatcher,
130 await discovery.discover_statics()
132 if wemo_data.discovery_enabled:
133 await discovery.async_discover_and_schedule()
139 """Unload a wemo config entry."""
140 _LOGGER.debug(
"Unloading WeMo")
143 wemo_data.config_entry_data.discovery.async_stop_discovery()
145 dispatcher = wemo_data.config_entry_data.dispatcher
146 if unload_ok := await dispatcher.async_unload_platforms(hass):
147 for coordinator
in list(
148 wemo_data.config_entry_data.device_coordinators.values()
150 await coordinator.async_shutdown()
151 assert not wemo_data.config_entry_data.device_coordinators
152 wemo_data.config_entry_data =
None
158 dispatch: DispatchCallback,
160 """Connect a wemo platform with the WemoDispatcher."""
161 module = dispatch.__module__
162 platform =
Platform(module.rsplit(
".", 1)[1])
165 await dispatcher.async_connect_platform(platform, dispatch)
169 """Dispatch WeMo devices to the correct platform."""
171 def __init__(self, config_entry: ConfigEntry) ->
None:
172 """Initialize the WemoDispatcher."""
174 self._added_serial_numbers: set[str] = set()
175 self._failed_serial_numbers: set[str] = set()
176 self._dispatch_backlog: dict[Platform, list[DeviceCoordinator]] = {}
177 self._dispatch_callbacks: dict[Platform, DispatchCallback] = {}
180 self, hass: HomeAssistant, wemo: pywemo.WeMoDevice
182 """Add a WeMo device to hass if it has not already been added."""
183 if wemo.serial_number
in self._added_serial_numbers:
188 except pywemo.PyWeMoException
as err:
189 if wemo.serial_number
not in self._failed_serial_numbers:
190 self._failed_serial_numbers.
add(wemo.serial_number)
192 "Unable to add WeMo %s %s: %s", repr(wemo), wemo.host, err
196 platforms = set(WEMO_MODEL_DISPATCH.get(wemo.model_name, [Platform.SWITCH]))
197 platforms.add(Platform.SENSOR)
198 platforms_to_load: list[Platform] = []
199 for platform
in platforms:
205 if platform
in self._dispatch_callbacks:
206 await self._dispatch_callbacks[platform](coordinator)
207 elif platform
in self._dispatch_backlog:
208 self._dispatch_backlog[platform].append(coordinator)
210 self._dispatch_backlog[platform] = [coordinator]
211 platforms_to_load.append(platform)
213 self._added_serial_numbers.
add(wemo.serial_number)
214 self._failed_serial_numbers.discard(wemo.serial_number)
216 if platforms_to_load:
217 await hass.config_entries.async_forward_entry_setups(
222 self, platform: Platform, dispatch: DispatchCallback
224 """Consider a platform as loaded and dispatch any backlog of discovered devices."""
225 self._dispatch_callbacks[platform] = dispatch
230 dispatch(coordinator)
231 for coordinator
in self._dispatch_backlog.pop(platform)
236 """Forward the unloading of an entry to platforms."""
237 platforms: set[Platform] = set(self._dispatch_backlog.keys())
238 platforms.update(self._dispatch_callbacks.keys())
239 return await hass.config_entries.async_unload_platforms(
245 """Use SSDP to discover WeMo devices."""
247 ADDITIONAL_SECONDS_BETWEEN_SCANS = 10
248 MAX_SECONDS_BETWEEN_SCANS = 300
253 wemo_dispatcher: WemoDispatcher,
254 static_config: Sequence[HostPortTuple],
257 """Initialize the WemoDiscovery."""
260 self.
_stop_stop: CALLBACK_TYPE |
None =
None
263 self.
_discover_job_discover_job: HassJob[[datetime],
None] |
None =
None
267 self, event_time: datetime |
None =
None
269 """Periodically scan the network looking for WeMo devices."""
270 _LOGGER.debug(
"Scanning network for WeMo devices")
272 for device
in await self.
_hass_hass.async_add_executor_job(
273 pywemo.discover_devices
294 """Run the periodic background scanning."""
295 self.
_entry_entry.async_create_background_task(
298 name=
"wemo_discovery",
304 """Stop the periodic background scanning."""
307 self.
_stop_stop =
None
310 """Initialize or Re-Initialize connections to statically configured devices."""
313 _LOGGER.debug(
"Adding statically configured WeMo devices")
317 self.
_hass_hass.async_add_executor_job(validate_static_config, host, port)
326 """Handle a static config."""
327 url = pywemo.setup_url_for_address(host, port)
331 "Unable to get description url for WeMo at: %s",
332 f
"{host}:{port}" if port
else host,
337 device = pywemo.discovery.device_from_description(url)
339 pywemo.exceptions.ActionException,
340 pywemo.exceptions.HTTPException,
342 _LOGGER.error(
"Unable to access WeMo at %s (%s)", url, err)
None async_stop_discovery(self)
int ADDITIONAL_SECONDS_BETWEEN_SCANS
int MAX_SECONDS_BETWEEN_SCANS
None async_discover_and_schedule(self, datetime|None event_time=None)
None __init__(self, HomeAssistant hass, WemoDispatcher wemo_dispatcher, Sequence[HostPortTuple] static_config, ConfigEntry entry)
None _async_discover_and_schedule_callback(self, datetime event_time)
None discover_statics(self)
None __init__(self, ConfigEntry config_entry)
None async_connect_platform(self, Platform platform, DispatchCallback dispatch)
None async_add_unique_device(self, HomeAssistant hass, pywemo.WeMoDevice wemo)
bool async_unload_platforms(self, HomeAssistant hass)
bool add(self, _T matcher)
DeviceCoordinator async_register_device(HomeAssistant hass, ConfigEntry config_entry, WeMoDevice wemo)
WemoData async_wemo_data(HomeAssistant hass)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
pywemo.WeMoDevice|None validate_static_config(str host, int|None port)
HostPortTuple coerce_host_port(str value)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None async_wemo_dispatcher_connect(HomeAssistant hass, DispatchCallback dispatch)
bool async_setup(HomeAssistant hass, ConfigType config)
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)
Any gather_with_limited_concurrency(int limit, *Any tasks, bool return_exceptions=False)