Add config flow to Wake on LAN (#121605)

This commit is contained in:
G Johansson 2024-07-19 21:20:43 +02:00 committed by GitHub
parent 7e0970e917
commit 288faf48e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 483 additions and 22 deletions

View File

@ -6,12 +6,13 @@ import logging
import voluptuous as vol import voluptuous as vol
import wakeonlan import wakeonlan
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,7 +44,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if broadcast_port is not None: if broadcast_port is not None:
service_kwargs["port"] = broadcast_port service_kwargs["port"] = broadcast_port
_LOGGER.info( _LOGGER.debug(
"Send magic packet to mac %s (broadcast: %s, port: %s)", "Send magic packet to mac %s (broadcast: %s, port: %s)",
mac_address, mac_address,
broadcast_address, broadcast_address,
@ -62,3 +63,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
) )
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Wake on LAN component entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -0,0 +1,87 @@
"""Support for button entity in wake on lan."""
from __future__ import annotations
from functools import partial
import logging
from typing import Any
import wakeonlan
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Wake on LAN sensor entry."""
broadcast_address: str | None = entry.options.get(CONF_BROADCAST_ADDRESS)
broadcast_port: int | None = entry.options.get(CONF_BROADCAST_PORT)
mac_address: str = entry.options[CONF_MAC]
name: str = entry.title
async_add_entities(
[
WolSwitch(
name,
mac_address,
broadcast_address,
broadcast_port,
)
]
)
class WolSwitch(ButtonEntity):
"""Representation of a wake on lan button."""
_attr_name = None
def __init__(
self,
name: str,
mac_address: str,
broadcast_address: str | None,
broadcast_port: int | None,
) -> None:
"""Initialize the WOL button."""
self._mac_address = mac_address
self._broadcast_address = broadcast_address
self._broadcast_port = broadcast_port
self._attr_unique_id = dr.format_mac(mac_address)
self._attr_device_info = dr.DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)},
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer="Wake on LAN",
name=name,
)
async def async_press(self) -> None:
"""Press the button."""
service_kwargs: dict[str, Any] = {}
if self._broadcast_address is not None:
service_kwargs["ip_address"] = self._broadcast_address
if self._broadcast_port is not None:
service_kwargs["port"] = self._broadcast_port
_LOGGER.debug(
"Send magic packet to mac %s (broadcast: %s, port: %s)",
self._mac_address,
self._broadcast_address,
self._broadcast_port,
)
await self.hass.async_add_executor_job(
partial(wakeonlan.send_magic_packet, self._mac_address, **service_kwargs)
)

View File

@ -0,0 +1,80 @@
"""Config flow for Wake on lan integration."""
from collections.abc import Mapping
from typing import Any
import voluptuous as vol
from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
SchemaFlowFormStep,
)
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
TextSelector,
)
from .const import DEFAULT_NAME, DOMAIN
async def validate(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate input setup."""
user_input = await validate_options(handler, user_input)
user_input[CONF_MAC] = dr.format_mac(user_input[CONF_MAC])
# Mac address needs to be unique
handler.parent_handler._async_abort_entries_match({CONF_MAC: user_input[CONF_MAC]}) # noqa: SLF001
return user_input
async def validate_options(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate input options."""
if CONF_BROADCAST_PORT in user_input:
# Convert float to int for broadcast port
user_input[CONF_BROADCAST_PORT] = int(user_input[CONF_BROADCAST_PORT])
return user_input
DATA_SCHEMA = {vol.Required(CONF_MAC): TextSelector()}
OPTIONS_SCHEMA = {
vol.Optional(CONF_BROADCAST_ADDRESS): TextSelector(),
vol.Optional(CONF_BROADCAST_PORT): NumberSelector(
NumberSelectorConfig(min=0, max=65535, step=1, mode=NumberSelectorMode.BOX)
),
}
CONFIG_FLOW = {
"user": SchemaFlowFormStep(
schema=vol.Schema(DATA_SCHEMA).extend(OPTIONS_SCHEMA),
validate_user_input=validate,
)
}
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
vol.Schema(OPTIONS_SCHEMA), validate_user_input=validate_options
),
}
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for Statistics."""
config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
mac: str = options[CONF_MAC]
return f"{DEFAULT_NAME} {mac}"

View File

@ -1,3 +1,11 @@
"""Constants for the Wake-On-LAN component.""" """Constants for the Wake-On-LAN component."""
from homeassistant.const import Platform
DOMAIN = "wake_on_lan" DOMAIN = "wake_on_lan"
PLATFORMS = [Platform.BUTTON]
CONF_OFF_ACTION = "turn_off"
DEFAULT_NAME = "Wake on LAN"
DEFAULT_PING_TIMEOUT = 1

View File

@ -2,6 +2,7 @@
"domain": "wake_on_lan", "domain": "wake_on_lan",
"name": "Wake on LAN", "name": "Wake on LAN",
"codeowners": ["@ntilley905"], "codeowners": ["@ntilley905"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["wakeonlan==2.1.0"] "requirements": ["wakeonlan==2.1.0"]

View File

@ -1,20 +1,56 @@
{ {
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"step": {
"user": {
"data": {
"mac": "MAC address",
"broadcast_address": "Broadcast address",
"broadcast_port": "Broadcast port"
},
"data_description": {
"mac": "MAC address of the device to wake up.",
"broadcast_address": "The IP address of the host to send the magic packet to. Defaults to `255.255.255.255` and is normally not changed.",
"broadcast_port": "The port to send the magic packet to. Defaults to `9` and is normally not changed."
}
}
}
},
"options": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"step": {
"init": {
"data": {
"broadcast_address": "[%key:component::wake_on_lan::config::step::user::data::broadcast_address%]",
"broadcast_port": "[%key:component::wake_on_lan::config::step::user::data::broadcast_port%]"
},
"data_description": {
"broadcast_address": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_address%]",
"broadcast_port": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_port%]"
}
}
}
},
"services": { "services": {
"send_magic_packet": { "send_magic_packet": {
"name": "Send magic packet", "name": "Send magic packet",
"description": "Sends a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities.", "description": "Sends a 'magic packet' to wake up a device with 'Wake-On-LAN' capabilities.",
"fields": { "fields": {
"mac": { "mac": {
"name": "MAC address", "name": "[%key:component::wake_on_lan::config::step::user::data::mac%]",
"description": "MAC address of the device to wake up." "description": "[%key:component::wake_on_lan::config::step::user::data_description::mac%]"
}, },
"broadcast_address": { "broadcast_address": {
"name": "Broadcast address", "name": "[%key:component::wake_on_lan::config::step::user::data::broadcast_address%]",
"description": "Broadcast IP where to send the magic packet." "description": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_address%]"
}, },
"broadcast_port": { "broadcast_port": {
"name": "Broadcast port", "name": "[%key:component::wake_on_lan::config::step::user::data::broadcast_port%]",
"description": "Port where to send the magic packet." "description": "[%key:component::wake_on_lan::config::step::user::data_description::broadcast_port%]"
} }
} }
} }

View File

@ -27,15 +27,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import CONF_OFF_ACTION, DEFAULT_NAME, DEFAULT_PING_TIMEOUT, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_OFF_ACTION = "turn_off"
DEFAULT_NAME = "Wake on LAN"
DEFAULT_PING_TIMEOUT = 1
PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_MAC): cv.string, vol.Required(CONF_MAC): cv.string,
@ -48,10 +43,10 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
) )
def setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up a wake on lan switch.""" """Set up a wake on lan switch."""
@ -62,7 +57,7 @@ def setup_platform(
name: str = config[CONF_NAME] name: str = config[CONF_NAME]
off_action: list[Any] | None = config.get(CONF_OFF_ACTION) off_action: list[Any] | None = config.get(CONF_OFF_ACTION)
add_entities( async_add_entities(
[ [
WolSwitch( WolSwitch(
hass, hass,

View File

@ -625,6 +625,7 @@ FLOWS = {
"volumio", "volumio",
"volvooncall", "volvooncall",
"vulcan", "vulcan",
"wake_on_lan",
"wallbox", "wallbox",
"waqi", "waqi",
"watttime", "watttime",

View File

@ -6743,7 +6743,7 @@
"wake_on_lan": { "wake_on_lan": {
"name": "Wake on LAN", "name": "Wake on LAN",
"integration_type": "hub", "integration_type": "hub",
"config_flow": false, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push"
}, },
"wallbox": { "wallbox": {

View File

@ -3,13 +3,23 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator from collections.abc import Generator
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from homeassistant.components.wake_on_lan.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
DEFAULT_MAC = "00:01:02:03:04:05"
@pytest.fixture @pytest.fixture
def mock_send_magic_packet() -> AsyncMock: def mock_send_magic_packet() -> Generator[AsyncMock]:
"""Mock magic packet.""" """Mock magic packet."""
with patch("wakeonlan.send_magic_packet") as mock_send: with patch("wakeonlan.send_magic_packet") as mock_send:
yield mock_send yield mock_send
@ -27,3 +37,48 @@ def mock_subprocess_call(subprocess_call_return_value: int) -> Generator[MagicMo
with patch("homeassistant.components.wake_on_lan.switch.sp.call") as mock_sp: with patch("homeassistant.components.wake_on_lan.switch.sp.call") as mock_sp:
mock_sp.return_value = subprocess_call_return_value mock_sp.return_value = subprocess_call_return_value
yield mock_sp yield mock_sp
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Automatically path uuid generator."""
with patch(
"homeassistant.components.wake_on_lan.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture(name="get_config")
async def get_config_to_integration_load() -> dict[str, Any]:
"""Return configuration.
To override the config, tests can be marked with:
@pytest.mark.parametrize("get_config", [{...}])
"""
return {
CONF_MAC: DEFAULT_MAC,
CONF_BROADCAST_ADDRESS: "255.255.255.255",
CONF_BROADCAST_PORT: 9,
}
@pytest.fixture(name="loaded_entry")
async def load_integration(
hass: HomeAssistant, get_config: dict[str, Any]
) -> MockConfigEntry:
"""Set up the Statistics integration in Home Assistant."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title=f"Wake on LAN {DEFAULT_MAC}",
source=SOURCE_USER,
options=get_config,
entry_id="1",
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry

View File

@ -0,0 +1,54 @@
"""The tests for the wake on lan button platform."""
from __future__ import annotations
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry
async def test_state(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
loaded_entry: MockConfigEntry,
) -> None:
"""Test button default state."""
state = hass.states.get("button.wake_on_lan_00_01_02_03_04_05")
assert state is not None
assert state.state == STATE_UNKNOWN
entry = entity_registry.async_get("button.wake_on_lan_00_01_02_03_04_05")
assert entry
assert entry.unique_id == "00:01:02:03:04:05"
async def test_service_calls(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
loaded_entry: MockConfigEntry,
mock_send_magic_packet: AsyncMock,
) -> None:
"""Test service call."""
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00")
freezer.move_to(now)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.wake_on_lan_00_01_02_03_04_05"},
blocking=True,
)
assert (
hass.states.get("button.wake_on_lan_00_01_02_03_04_05").state == now.isoformat()
)

View File

@ -0,0 +1,109 @@
"""Test the Scrape config flow."""
from __future__ import annotations
from unittest.mock import AsyncMock
from homeassistant import config_entries
from homeassistant.components.wake_on_lan.const import DOMAIN
from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_BROADCAST_PORT, CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import DEFAULT_MAC
from tests.common import MockConfigEntry
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_MAC: DEFAULT_MAC,
CONF_BROADCAST_ADDRESS: "255.255.255.255",
CONF_BROADCAST_PORT: 9,
},
)
await hass.async_block_till_done(wait_background_tasks=True)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["version"] == 1
assert result["options"] == {
CONF_MAC: DEFAULT_MAC,
CONF_BROADCAST_ADDRESS: "255.255.255.255",
CONF_BROADCAST_PORT: 9,
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_options_flow(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test options flow."""
result = await hass.config_entries.options.async_init(loaded_entry.entry_id)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
CONF_BROADCAST_ADDRESS: "192.168.255.255",
CONF_BROADCAST_PORT: 10,
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_MAC: DEFAULT_MAC,
CONF_BROADCAST_ADDRESS: "192.168.255.255",
CONF_BROADCAST_PORT: 10,
}
await hass.async_block_till_done()
assert loaded_entry.options == {
CONF_MAC: DEFAULT_MAC,
CONF_BROADCAST_ADDRESS: "192.168.255.255",
CONF_BROADCAST_PORT: 10,
}
# Check the entity was updated, no new entity was created
assert len(hass.states.async_all()) == 1
state = hass.states.get("button.wake_on_lan_00_01_02_03_04_05")
assert state is not None
async def test_entry_already_exist(
hass: HomeAssistant, loaded_entry: MockConfigEntry
) -> None:
"""Test abort when entry already exist."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "user"
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_MAC: DEFAULT_MAC,
CONF_BROADCAST_ADDRESS: "255.255.255.255",
CONF_BROADCAST_PORT: 9,
},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -8,9 +8,21 @@ import pytest
import voluptuous as vol import voluptuous as vol
from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
"""Test unload an entry."""
assert loaded_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(loaded_entry.entry_id)
await hass.async_block_till_done()
assert loaded_entry.state is ConfigEntryState.NOT_LOADED
async def test_send_magic_packet(hass: HomeAssistant) -> None: async def test_send_magic_packet(hass: HomeAssistant) -> None:
"""Test of send magic packet service call.""" """Test of send magic packet service call."""

View File

@ -13,6 +13,7 @@ from homeassistant.const import (
STATE_ON, STATE_ON,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_mock_service from tests.common import async_mock_service
@ -64,7 +65,7 @@ async def test_broadcast_config_ip_and_port(
hass: HomeAssistant, mock_send_magic_packet: AsyncMock hass: HomeAssistant, mock_send_magic_packet: AsyncMock
) -> None: ) -> None:
"""Test with broadcast address and broadcast port config.""" """Test with broadcast address and broadcast port config."""
mac = "00-01-02-03-04-05" mac = "00:01:02:03:04:05"
broadcast_address = "255.255.255.255" broadcast_address = "255.255.255.255"
port = 999 port = 999
@ -92,6 +93,7 @@ async def test_broadcast_config_ip_and_port(
blocking=True, blocking=True,
) )
mac = dr.format_mac(mac)
mock_send_magic_packet.assert_called_with( mock_send_magic_packet.assert_called_with(
mac, ip_address=broadcast_address, port=port mac, ip_address=broadcast_address, port=port
) )
@ -102,7 +104,7 @@ async def test_broadcast_config_ip(
) -> None: ) -> None:
"""Test with only broadcast address.""" """Test with only broadcast address."""
mac = "00-01-02-03-04-05" mac = "00:01:02:03:04:05"
broadcast_address = "255.255.255.255" broadcast_address = "255.255.255.255"
assert await async_setup_component( assert await async_setup_component(
@ -128,6 +130,7 @@ async def test_broadcast_config_ip(
blocking=True, blocking=True,
) )
mac = dr.format_mac(mac)
mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address) mock_send_magic_packet.assert_called_with(mac, ip_address=broadcast_address)
@ -136,7 +139,7 @@ async def test_broadcast_config_port(
) -> None: ) -> None:
"""Test with only broadcast port config.""" """Test with only broadcast port config."""
mac = "00-01-02-03-04-05" mac = "00:01:02:03:04:05"
port = 999 port = 999
assert await async_setup_component( assert await async_setup_component(
@ -156,6 +159,7 @@ async def test_broadcast_config_port(
blocking=True, blocking=True,
) )
mac = dr.format_mac(mac)
mock_send_magic_packet.assert_called_with(mac, port=port) mock_send_magic_packet.assert_called_with(mac, port=port)