├── wakeonlan ├── py.typed └── __init__.py ├── docs ├── license.rst ├── readme.rst ├── requirements.txt ├── apidocs.rst ├── index.rst └── conf.py ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.rst ├── pyproject.toml ├── .github └── workflows │ └── ci.yaml ├── README.rst ├── poetry.lock └── test_wakeonlan.py /wakeonlan/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../LICENSE.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | *.lcov 3 | *cache*/ 4 | _build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /docs/apidocs.rst: -------------------------------------------------------------------------------- 1 | ############### 2 | Python api docs 3 | ############### 4 | 5 | .. automodule:: wakeonlan 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | Pywakeonlan 3 | ########### 4 | 5 | .. toctree:: 6 | readme 7 | apidocs 8 | license 9 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: '3' 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | fail_on_warning: true 11 | 12 | formats: 13 | - pdf 14 | - epub 15 | 16 | python: 17 | install: 18 | - requirements: docs/requirements.txt 19 | - method: pip 20 | path: . 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for the documentation generation. 3 | 4 | """ 5 | import pkg_resources 6 | 7 | 8 | project = 'wakeonlan' 9 | _dist = pkg_resources.get_distribution(project) 10 | 11 | version = _dist.version 12 | release = _dist.version 13 | copyright = '2012, Remco Haszing' 14 | 15 | 16 | extensions = [ 17 | 'sphinx.ext.autodoc', 18 | 'sphinx.ext.intersphinx', 19 | 'sphinx.ext.napoleon', 20 | 'sphinx_rtd_theme', 21 | ] 22 | 23 | intersphinx_mapping = { 24 | 'python': ('https://docs.python.org/3.11', None), 25 | } 26 | 27 | nitpick_ignore = [('py:class', 'socket.AddressFamily')] 28 | 29 | nitpicky = True 30 | 31 | default_role = 'any' 32 | todo_include_todos = True 33 | 34 | master_doc = 'index' 35 | html_theme = 'sphinx_rtd_theme' 36 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | MIT License 3 | ########### 4 | 5 | Copyright © 2012 Remco Haszing 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wakeonlan" 3 | version = "3.1.0" 4 | description = "A small python module for wake on lan." 5 | authors = ["Remco Haszing "] 6 | license = "MIT" 7 | readme = "README.rst" 8 | documentation = "http://pywakeonlan.readthedocs.io" 9 | repository = "https://github.com/remcohaszing/pywakeonlan" 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Intended Audience :: Developers", 13 | "Intended Audience :: System Administrators", 14 | "Topic :: System :: Networking", 15 | "Typing :: Typed" 16 | ] 17 | 18 | [tool.poetry.urls] 19 | Changelog = "https://github.com/remcohaszing/pywakeonlan/releases" 20 | Issues = "https://github.com/remcohaszing/pywakeonlan/issues" 21 | Sponsor = "https://github.com/sponsors/remcohaszing" 22 | 23 | [tool.poetry.scripts] 24 | wakeonlan = "wakeonlan:main" 25 | 26 | [tool.poetry.dependencies] 27 | python = "^3.7" 28 | 29 | [tool.poetry.dev-dependencies] 30 | coverage = { version = "*", extras = ["toml"] } 31 | mypy = { version = "*", python = ">=3.8" } 32 | ruff = { version = "*", python = ">=3.8" } 33 | types-setuptools = { version = "*", python = ">=3.8" } 34 | 35 | [tool.coverage.run] 36 | branch = true 37 | command_line = "-m unittest -v" 38 | source = ["wakeonlan"] 39 | 40 | [tool.coverage.report] 41 | show_missing = true 42 | 43 | [tool.mypy] 44 | check_untyped_defs = true 45 | disallow_incomplete_defs = true 46 | disallow_untyped_calls = true 47 | disallow_untyped_decorators = true 48 | disallow_untyped_defs = true 49 | ignore_missing_imports = true 50 | strict_optional = true 51 | warn_return_any = true 52 | 53 | [tool.ruff.format] 54 | quote-style = "single" 55 | 56 | [tool.ruff.lint] 57 | select = ["D", "E", "F", "I", "N", "W"] 58 | ignore = ["D200", "D212", "E501", "W505"] 59 | 60 | [tool.ruff.lint.isort] 61 | lines-after-imports = 2 62 | 63 | [tool.ruff.lint.pydocstyle] 64 | convention = "google" 65 | 66 | [build-system] 67 | requires = ["poetry-core"] 68 | build-backend = "poetry.core.masonry.api" 69 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | tags: ['*'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: '3.10' 17 | - run: pip install poetry 18 | - run: poetry build 19 | - uses: actions/upload-artifact@v3 20 | with: 21 | name: dist 22 | path: dist/ 23 | 24 | mypy: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-python@v4 29 | with: 30 | python-version: '3.10' 31 | - run: pip install poetry 32 | - run: poetry install 33 | - run: poetry run mypy . 34 | 35 | ruff: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-python@v4 40 | with: 41 | python-version: '3.10' 42 | - run: pip install poetry 43 | - run: poetry install 44 | - run: poetry run ruff check 45 | - run: poetry run ruff format --check 46 | 47 | unittest: 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | python-version: 52 | - '3.7' 53 | - '3.8' 54 | - '3.9' 55 | - '3.10' 56 | - '3.11' 57 | - '3.12' 58 | steps: 59 | - uses: actions/checkout@v3 60 | - uses: actions/setup-python@v4 61 | with: 62 | python-version: ${{ matrix.python-version }} 63 | - run: pip install poetry 64 | - run: poetry install 65 | - run: poetry run coverage run 66 | - run: poetry run coverage report 67 | - run: poetry run coverage lcov 68 | - uses: codecov/codecov-action@v3 69 | if: ${{ matrix.python-version == '3.10' }} 70 | 71 | publish: 72 | runs-on: ubuntu-latest 73 | needs: 74 | - build 75 | - mypy 76 | - ruff 77 | - unittest 78 | if: startsWith(github.ref, 'refs/tags/') 79 | steps: 80 | - uses: actions/download-artifact@v3 81 | with: 82 | name: dist 83 | path: dist/ 84 | - uses: pypa/gh-action-pypi-publish@release/v1 85 | with: 86 | user: __token__ 87 | password: ${{ secrets.PYPI_API_TOKEN }} 88 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ######### 2 | wakeonlan 3 | ######### 4 | 5 | .. image:: https://img.shields.io/pypi/v/wakeonlan.svg 6 | :target: https://pypi.org/project/wakeonlan/ 7 | :alt: Pypi version 8 | 9 | .. image:: https://img.shields.io/pypi/pyversions/wakeonlan.svg 10 | :target: https://pypi.org/project/wakeonlan/#files 11 | :alt: Supported Python versions 12 | 13 | .. image:: https://github.com/remcohaszing/pywakeonlan/actions/workflows/ci.yaml/badge.svg 14 | :target: https://github.com/remcohaszing/pywakeonlan/actions/workflows/ci.yaml 15 | :alt: Build Status 16 | 17 | .. image:: https://readthedocs.org/projects/pywakeonlan/badge/?version=latest 18 | :target: https://pywakeonlan.readthedocs.io/en/latest 19 | :alt: Documentation Status 20 | 21 | .. image:: https://codecov.io/gh/remcohaszing/pywakeonlan/branch/master/graph/badge.svg 22 | :target: https://codecov.io/gh/remcohaszing/pywakeonlan 23 | :alt: Code coverage 24 | 25 | A small python module for wake on lan. 26 | 27 | For more information on the wake on lan protocol please take a look at 28 | `Wikipedia `_. 29 | 30 | ************ 31 | Installation 32 | ************ 33 | 34 | :: 35 | 36 | pip install wakeonlan 37 | 38 | 39 | ***** 40 | Usage 41 | ***** 42 | 43 | To wake up a computer using wake on lan it must first be enabled in the BIOS 44 | settings. Please note the computer you are trying to power on does not have an 45 | ip address, but it does have a mac address. The package needs to be sent as a 46 | broadcast package. 47 | 48 | 49 | As a python module 50 | ================== 51 | 52 | Import the module 53 | 54 | >>> from wakeonlan import send_magic_packet 55 | 56 | 57 | Wake up a single computer by its mac address 58 | 59 | >>> send_magic_packet('ff.ff.ff.ff.ff.ff') 60 | 61 | 62 | Wake up multiple computers by their mac addresses. 63 | 64 | >>> send_magic_packet('ff.ff.ff.ff.ff.ff', 65 | ... '00-00-00-00-00-00', 66 | ... 'FFFFFFFFFFFF') 67 | 68 | 69 | An external host may be specified. Do note that port forwarding on that host is 70 | required. The default ip address is 255.255.255.255 and the default port is 9. 71 | 72 | >>> send_magic_packet('ff.ff.ff.ff.ff.ff', 73 | ... ip_address='example.com', 74 | ... port=1337) 75 | 76 | 77 | A network adapter may be specified. The magic packet will be routed through this interface. 78 | 79 | >>> send_magic_packet('ff.ff.ff.ff.ff.ff', 80 | ... interface='192.168.0.2') 81 | 82 | 83 | As a standalone script 84 | ====================== 85 | 86 | :: 87 | 88 | usage: wakeonlan [-h] [-6] [-i IP] [-p PORT] [-n INTERFACE] mac address [mac address ...] 89 | 90 | Wake one or more computers using the wake on lan protocol. 91 | 92 | positional arguments: 93 | mac address The mac addresses of the computers you are trying to wake. 94 | 95 | options: 96 | -h, --help show this help message and exit 97 | -6, --ipv6 To indicate if ipv6 should be used by default instead of ipv4. (default: False) 98 | -i IP, --ip IP The ip address of the host to send the magic packet to. (default: 255.255.255.255) 99 | -p PORT, --port PORT The port of the host to send the magic packet to. (default: 9) 100 | -n INTERFACE, --interface INTERFACE 101 | The ip address of the network adapter to route the magic packet through. (default: None) 102 | 103 | 104 | ************ 105 | Dependencies 106 | ************ 107 | 108 | - Python 3.x 109 | 110 | 111 | ******* 112 | License 113 | ******* 114 | 115 | `MIT `_ © `Remco Haszing `_ 116 | -------------------------------------------------------------------------------- /wakeonlan/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Small module for use with the wake on lan protocol. 4 | 5 | """ 6 | import argparse 7 | import ipaddress 8 | import socket 9 | import typing 10 | 11 | 12 | BROADCAST_IP = '255.255.255.255' 13 | DEFAULT_PORT = 9 14 | 15 | 16 | def create_magic_packet(macaddress: str) -> bytes: 17 | """ 18 | Create a magic packet. 19 | 20 | A magic packet is a packet that can be used with the for wake on lan 21 | protocol to wake up a computer. The packet is constructed from the 22 | mac address given as a parameter. 23 | 24 | Args: 25 | macaddress: the mac address that should be parsed into a magic packet. 26 | 27 | """ 28 | if len(macaddress) == 17: 29 | sep = macaddress[2] 30 | macaddress = macaddress.replace(sep, '') 31 | elif len(macaddress) == 14: 32 | sep = macaddress[4] 33 | macaddress = macaddress.replace(sep, '') 34 | if len(macaddress) != 12: 35 | raise ValueError('Incorrect MAC address format') 36 | return bytes.fromhex('F' * 12 + macaddress * 16) 37 | 38 | 39 | def send_magic_packet( 40 | *macs: str, 41 | ip_address: str = BROADCAST_IP, 42 | port: int = DEFAULT_PORT, 43 | interface: typing.Optional[str] = None, 44 | address_family: typing.Optional[socket.AddressFamily] = None, 45 | ) -> None: 46 | """ 47 | Wake up computers having any of the given mac addresses. 48 | 49 | Wake on lan must be enabled on the host device. 50 | 51 | Args: 52 | macs: One or more macaddresses of machines to wake. 53 | 54 | Keyword Args: 55 | ip_address: the ip address of the host to send the magic packet 56 | to. 57 | port: the port of the host to send the magic packet to. 58 | interface: the ip address of the network adapter to route the 59 | magic packet through. 60 | address_family: the address family of the ip address to initiate 61 | connection with. When not specificied, chosen automatically 62 | between IPv4 and IPv6. 63 | 64 | """ 65 | packets = [create_magic_packet(mac) for mac in macs] 66 | 67 | if address_family is None: 68 | address_family = ( 69 | socket.AF_INET6 if _is_ipv6_address(ip_address) else socket.AF_INET 70 | ) 71 | 72 | with socket.socket(address_family, socket.SOCK_DGRAM) as sock: 73 | if interface is not None: 74 | sock.bind((interface, 0)) 75 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 76 | sock.connect((ip_address, port)) 77 | for packet in packets: 78 | sock.send(packet) 79 | 80 | 81 | def _is_ipv6_address(ip_address: str) -> bool: 82 | try: 83 | return isinstance(ipaddress.ip_address(ip_address), ipaddress.IPv6Address) 84 | except ValueError: 85 | return False 86 | 87 | 88 | def main(argv: typing.Optional[typing.List[str]] = None) -> None: 89 | """ 90 | Run wake on lan as a CLI application. 91 | 92 | """ 93 | parser = argparse.ArgumentParser( 94 | description='Wake one or more computers using the wake on lan protocol.', 95 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 96 | ) 97 | parser.add_argument( 98 | 'macs', 99 | metavar='mac address', 100 | nargs='+', 101 | help='The mac addresses of the computers you are trying to wake.', 102 | ) 103 | parser.add_argument( 104 | '-6', 105 | '--ipv6', 106 | action='store_true', 107 | help='To indicate if ipv6 should be used by default instead of ipv4.', 108 | ) 109 | parser.add_argument( 110 | '-i', 111 | '--ip', 112 | default=BROADCAST_IP, 113 | help='The ip address of the host to send the magic packet to.', 114 | ) 115 | parser.add_argument( 116 | '-p', 117 | '--port', 118 | type=int, 119 | default=DEFAULT_PORT, 120 | help='The port of the host to send the magic packet to.', 121 | ) 122 | parser.add_argument( 123 | '-n', 124 | '--interface', 125 | help='The ip address of the network adapter to route the magic packet through.', 126 | ) 127 | args = parser.parse_args(argv) 128 | send_magic_packet( 129 | *args.macs, 130 | ip_address=args.ip, 131 | port=args.port, 132 | interface=args.interface, 133 | address_family=socket.AF_INET6 if args.ipv6 else None, 134 | ) 135 | 136 | 137 | if __name__ == '__main__': # pragma: nocover 138 | main() 139 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "coverage" 5 | version = "7.2.7" 6 | description = "Code coverage measurement for Python" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, 11 | {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, 12 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, 13 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, 14 | {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, 15 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, 16 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, 17 | {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, 18 | {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, 19 | {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, 20 | {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, 21 | {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, 22 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, 23 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, 24 | {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, 25 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, 26 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, 27 | {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, 28 | {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, 29 | {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, 30 | {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, 31 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, 32 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, 33 | {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, 34 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, 35 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, 36 | {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, 37 | {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, 38 | {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, 39 | {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, 40 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, 41 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, 42 | {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, 43 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, 44 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, 45 | {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, 46 | {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, 47 | {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, 48 | {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, 49 | {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, 50 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, 51 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, 52 | {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, 53 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, 54 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, 55 | {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, 56 | {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, 57 | {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, 58 | {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, 59 | {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, 60 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, 61 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, 62 | {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, 63 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, 64 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, 65 | {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, 66 | {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, 67 | {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, 68 | {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, 69 | {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, 70 | ] 71 | 72 | [package.dependencies] 73 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 74 | 75 | [package.extras] 76 | toml = ["tomli"] 77 | 78 | [[package]] 79 | name = "mypy" 80 | version = "1.8.0" 81 | description = "Optional static typing for Python" 82 | optional = false 83 | python-versions = ">=3.8" 84 | files = [ 85 | {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, 86 | {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, 87 | {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, 88 | {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, 89 | {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, 90 | {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, 91 | {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, 92 | {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, 93 | {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, 94 | {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, 95 | {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, 96 | {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, 97 | {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, 98 | {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, 99 | {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, 100 | {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, 101 | {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, 102 | {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, 103 | {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, 104 | {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, 105 | {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, 106 | {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, 107 | {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, 108 | {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, 109 | {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, 110 | {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, 111 | {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, 112 | ] 113 | 114 | [package.dependencies] 115 | mypy-extensions = ">=1.0.0" 116 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 117 | typing-extensions = ">=4.1.0" 118 | 119 | [package.extras] 120 | dmypy = ["psutil (>=4.0)"] 121 | install-types = ["pip"] 122 | mypyc = ["setuptools (>=50)"] 123 | reports = ["lxml"] 124 | 125 | [[package]] 126 | name = "mypy-extensions" 127 | version = "1.0.0" 128 | description = "Type system extensions for programs checked with the mypy type checker." 129 | optional = false 130 | python-versions = ">=3.5" 131 | files = [ 132 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 133 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 134 | ] 135 | 136 | [[package]] 137 | name = "ruff" 138 | version = "0.2.1" 139 | description = "An extremely fast Python linter and code formatter, written in Rust." 140 | optional = false 141 | python-versions = ">=3.7" 142 | files = [ 143 | {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, 144 | {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, 145 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, 146 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, 147 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, 148 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, 149 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, 150 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, 151 | {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, 152 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, 153 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, 154 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, 155 | {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, 156 | {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, 157 | {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, 158 | {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, 159 | {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, 160 | ] 161 | 162 | [[package]] 163 | name = "tomli" 164 | version = "2.0.1" 165 | description = "A lil' TOML parser" 166 | optional = false 167 | python-versions = ">=3.7" 168 | files = [ 169 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 170 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 171 | ] 172 | 173 | [[package]] 174 | name = "types-setuptools" 175 | version = "69.1.0.20240217" 176 | description = "Typing stubs for setuptools" 177 | optional = false 178 | python-versions = ">=3.8" 179 | files = [ 180 | {file = "types-setuptools-69.1.0.20240217.tar.gz", hash = "sha256:243fecc8850b6f7fbfa84bab18ec93407046a4e91130056fd5a7caef971aaff9"}, 181 | {file = "types_setuptools-69.1.0.20240217-py3-none-any.whl", hash = "sha256:8b60e14a652b48bda292801c5a0c1251c190ad587c295f7839e901634913bb96"}, 182 | ] 183 | 184 | [[package]] 185 | name = "typing-extensions" 186 | version = "4.9.0" 187 | description = "Backported and Experimental Type Hints for Python 3.8+" 188 | optional = false 189 | python-versions = ">=3.8" 190 | files = [ 191 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 192 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 193 | ] 194 | 195 | [metadata] 196 | lock-version = "2.0" 197 | python-versions = "^3.7" 198 | content-hash = "74c93a8130750b8785bc13909607df54113a8fe9ee89667513ff5d38b7ec9339" 199 | -------------------------------------------------------------------------------- /test_wakeonlan.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for wakeonlan. 3 | 4 | """ 5 | import socket 6 | import unittest 7 | from unittest import mock 8 | 9 | from wakeonlan import create_magic_packet, main, send_magic_packet 10 | 11 | 12 | class TestCreateMagicPacket(unittest.TestCase): 13 | """ 14 | Test :ref:`create_magic_packet`. 15 | 16 | """ 17 | 18 | def test_no_separators(self) -> None: 19 | """ 20 | Test without separators. 21 | 22 | """ 23 | result = create_magic_packet('000000000000') 24 | self.assertEqual( 25 | result, 26 | b'\xff\xff\xff\xff\xff\xff' 27 | b'\x00\x00\x00\x00\x00\x00' 28 | b'\x00\x00\x00\x00\x00\x00' 29 | b'\x00\x00\x00\x00\x00\x00' 30 | b'\x00\x00\x00\x00\x00\x00' 31 | b'\x00\x00\x00\x00\x00\x00' 32 | b'\x00\x00\x00\x00\x00\x00' 33 | b'\x00\x00\x00\x00\x00\x00' 34 | b'\x00\x00\x00\x00\x00\x00' 35 | b'\x00\x00\x00\x00\x00\x00' 36 | b'\x00\x00\x00\x00\x00\x00' 37 | b'\x00\x00\x00\x00\x00\x00' 38 | b'\x00\x00\x00\x00\x00\x00' 39 | b'\x00\x00\x00\x00\x00\x00' 40 | b'\x00\x00\x00\x00\x00\x00' 41 | b'\x00\x00\x00\x00\x00\x00' 42 | b'\x00\x00\x00\x00\x00\x00', 43 | ) 44 | 45 | def test_colon(self) -> None: 46 | """ 47 | Test with a colon as separator. 48 | 49 | """ 50 | result = create_magic_packet('01:23:45:67:89:ab') 51 | self.assertEqual( 52 | result, 53 | b'\xff\xff\xff\xff\xff\xff' 54 | b'\x01#Eg\x89\xab' 55 | b'\x01#Eg\x89\xab' 56 | b'\x01#Eg\x89\xab' 57 | b'\x01#Eg\x89\xab' 58 | b'\x01#Eg\x89\xab' 59 | b'\x01#Eg\x89\xab' 60 | b'\x01#Eg\x89\xab' 61 | b'\x01#Eg\x89\xab' 62 | b'\x01#Eg\x89\xab' 63 | b'\x01#Eg\x89\xab' 64 | b'\x01#Eg\x89\xab' 65 | b'\x01#Eg\x89\xab' 66 | b'\x01#Eg\x89\xab' 67 | b'\x01#Eg\x89\xab' 68 | b'\x01#Eg\x89\xab' 69 | b'\x01#Eg\x89\xab', 70 | ) 71 | 72 | def test_hyphen(self) -> None: 73 | """ 74 | Test with a hyphen as separator. 75 | 76 | """ 77 | result = create_magic_packet('ff-ff-ff-ff-ff-ff') 78 | self.assertEqual( 79 | result, 80 | b'\xff\xff\xff\xff\xff\xff' 81 | b'\xff\xff\xff\xff\xff\xff' 82 | b'\xff\xff\xff\xff\xff\xff' 83 | b'\xff\xff\xff\xff\xff\xff' 84 | b'\xff\xff\xff\xff\xff\xff' 85 | b'\xff\xff\xff\xff\xff\xff' 86 | b'\xff\xff\xff\xff\xff\xff' 87 | b'\xff\xff\xff\xff\xff\xff' 88 | b'\xff\xff\xff\xff\xff\xff' 89 | b'\xff\xff\xff\xff\xff\xff' 90 | b'\xff\xff\xff\xff\xff\xff' 91 | b'\xff\xff\xff\xff\xff\xff' 92 | b'\xff\xff\xff\xff\xff\xff' 93 | b'\xff\xff\xff\xff\xff\xff' 94 | b'\xff\xff\xff\xff\xff\xff' 95 | b'\xff\xff\xff\xff\xff\xff' 96 | b'\xff\xff\xff\xff\xff\xff', 97 | ) 98 | 99 | def test_dot(self) -> None: 100 | """ 101 | Test with a dot as separator. 102 | 103 | """ 104 | result = create_magic_packet('ffff.ffff.ffff') 105 | self.assertEqual( 106 | result, 107 | b'\xff\xff\xff\xff\xff\xff' 108 | b'\xff\xff\xff\xff\xff\xff' 109 | b'\xff\xff\xff\xff\xff\xff' 110 | b'\xff\xff\xff\xff\xff\xff' 111 | b'\xff\xff\xff\xff\xff\xff' 112 | b'\xff\xff\xff\xff\xff\xff' 113 | b'\xff\xff\xff\xff\xff\xff' 114 | b'\xff\xff\xff\xff\xff\xff' 115 | b'\xff\xff\xff\xff\xff\xff' 116 | b'\xff\xff\xff\xff\xff\xff' 117 | b'\xff\xff\xff\xff\xff\xff' 118 | b'\xff\xff\xff\xff\xff\xff' 119 | b'\xff\xff\xff\xff\xff\xff' 120 | b'\xff\xff\xff\xff\xff\xff' 121 | b'\xff\xff\xff\xff\xff\xff' 122 | b'\xff\xff\xff\xff\xff\xff' 123 | b'\xff\xff\xff\xff\xff\xff', 124 | ) 125 | 126 | def test_invalid(self) -> None: 127 | """ 128 | Test an invalid mac address. 129 | 130 | """ 131 | with self.assertRaises(ValueError): 132 | create_magic_packet('invalid') 133 | 134 | 135 | class TestSendMagicPacket(unittest.TestCase): 136 | """ 137 | Test :ref:`send_magic_packet`. 138 | 139 | """ 140 | 141 | @mock.patch('socket.socket') 142 | def test_send_magic_packet(self, sock: mock.Mock) -> None: 143 | """ 144 | Test whether the magic packets are broadcasted to the specified network. 145 | 146 | """ 147 | send_magic_packet( 148 | '133713371337', '00-00-00-00-00-00', ip_address='example.com', port=7 149 | ) 150 | self.assertEqual( 151 | sock.mock_calls, 152 | [ 153 | mock.call(socket.AF_INET, socket.SOCK_DGRAM), 154 | mock.call().__enter__(), 155 | mock.call() 156 | .__enter__() 157 | .setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1), 158 | mock.call().__enter__().connect(('example.com', 7)), 159 | mock.call() 160 | .__enter__() 161 | .send( 162 | b'\xff\xff\xff\xff\xff\xff' 163 | b'\x137\x137\x137' 164 | b'\x137\x137\x137' 165 | b'\x137\x137\x137' 166 | b'\x137\x137\x137' 167 | b'\x137\x137\x137' 168 | b'\x137\x137\x137' 169 | b'\x137\x137\x137' 170 | b'\x137\x137\x137' 171 | b'\x137\x137\x137' 172 | b'\x137\x137\x137' 173 | b'\x137\x137\x137' 174 | b'\x137\x137\x137' 175 | b'\x137\x137\x137' 176 | b'\x137\x137\x137' 177 | b'\x137\x137\x137' 178 | b'\x137\x137\x137' 179 | ), 180 | mock.call() 181 | .__enter__() 182 | .send( 183 | b'\xff\xff\xff\xff\xff\xff' 184 | b'\x00\x00\x00\x00\x00\x00' 185 | b'\x00\x00\x00\x00\x00\x00' 186 | b'\x00\x00\x00\x00\x00\x00' 187 | b'\x00\x00\x00\x00\x00\x00' 188 | b'\x00\x00\x00\x00\x00\x00' 189 | b'\x00\x00\x00\x00\x00\x00' 190 | b'\x00\x00\x00\x00\x00\x00' 191 | b'\x00\x00\x00\x00\x00\x00' 192 | b'\x00\x00\x00\x00\x00\x00' 193 | b'\x00\x00\x00\x00\x00\x00' 194 | b'\x00\x00\x00\x00\x00\x00' 195 | b'\x00\x00\x00\x00\x00\x00' 196 | b'\x00\x00\x00\x00\x00\x00' 197 | b'\x00\x00\x00\x00\x00\x00' 198 | b'\x00\x00\x00\x00\x00\x00' 199 | b'\x00\x00\x00\x00\x00\x00' 200 | ), 201 | mock.call().__exit__(None, None, None), 202 | ], 203 | ) 204 | 205 | @mock.patch('socket.socket') 206 | def test_send_magic_packet_default(self, sock: mock.Mock) -> None: 207 | """ 208 | Test whether the magic packets are broadcasted using default values. 209 | 210 | """ 211 | send_magic_packet('133713371337', '00-00-00-00-00-00') 212 | self.assertEqual( 213 | sock.mock_calls, 214 | [ 215 | mock.call(socket.AF_INET, socket.SOCK_DGRAM), 216 | mock.call().__enter__(), 217 | mock.call() 218 | .__enter__() 219 | .setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1), 220 | mock.call().__enter__().connect(('255.255.255.255', 9)), 221 | mock.call() 222 | .__enter__() 223 | .send( 224 | b'\xff\xff\xff\xff\xff\xff' 225 | b'\x137\x137\x137' 226 | b'\x137\x137\x137' 227 | b'\x137\x137\x137' 228 | b'\x137\x137\x137' 229 | b'\x137\x137\x137' 230 | b'\x137\x137\x137' 231 | b'\x137\x137\x137' 232 | b'\x137\x137\x137' 233 | b'\x137\x137\x137' 234 | b'\x137\x137\x137' 235 | b'\x137\x137\x137' 236 | b'\x137\x137\x137' 237 | b'\x137\x137\x137' 238 | b'\x137\x137\x137' 239 | b'\x137\x137\x137' 240 | b'\x137\x137\x137' 241 | ), 242 | mock.call() 243 | .__enter__() 244 | .send( 245 | b'\xff\xff\xff\xff\xff\xff' 246 | b'\x00\x00\x00\x00\x00\x00' 247 | b'\x00\x00\x00\x00\x00\x00' 248 | b'\x00\x00\x00\x00\x00\x00' 249 | b'\x00\x00\x00\x00\x00\x00' 250 | b'\x00\x00\x00\x00\x00\x00' 251 | b'\x00\x00\x00\x00\x00\x00' 252 | b'\x00\x00\x00\x00\x00\x00' 253 | b'\x00\x00\x00\x00\x00\x00' 254 | b'\x00\x00\x00\x00\x00\x00' 255 | b'\x00\x00\x00\x00\x00\x00' 256 | b'\x00\x00\x00\x00\x00\x00' 257 | b'\x00\x00\x00\x00\x00\x00' 258 | b'\x00\x00\x00\x00\x00\x00' 259 | b'\x00\x00\x00\x00\x00\x00' 260 | b'\x00\x00\x00\x00\x00\x00' 261 | b'\x00\x00\x00\x00\x00\x00' 262 | ), 263 | mock.call().__exit__(None, None, None), 264 | ], 265 | ) 266 | 267 | @mock.patch('socket.socket') 268 | def test_send_magic_packet_interface(self, sock: mock.Mock) -> None: 269 | """ 270 | Test whether the magic packets are broadcasted to the specified network via specified interface. 271 | 272 | """ 273 | send_magic_packet( 274 | '133713371337', 275 | '00-00-00-00-00-00', 276 | ip_address='example.com', 277 | port=7, 278 | interface='192.168.0.2', 279 | ) 280 | self.assertEqual( 281 | sock.mock_calls, 282 | [ 283 | mock.call(socket.AF_INET, socket.SOCK_DGRAM), 284 | mock.call().__enter__(), 285 | mock.call().__enter__().bind(('192.168.0.2', 0)), 286 | mock.call() 287 | .__enter__() 288 | .setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1), 289 | mock.call().__enter__().connect(('example.com', 7)), 290 | mock.call() 291 | .__enter__() 292 | .send( 293 | b'\xff\xff\xff\xff\xff\xff' 294 | b'\x137\x137\x137' 295 | b'\x137\x137\x137' 296 | b'\x137\x137\x137' 297 | b'\x137\x137\x137' 298 | b'\x137\x137\x137' 299 | b'\x137\x137\x137' 300 | b'\x137\x137\x137' 301 | b'\x137\x137\x137' 302 | b'\x137\x137\x137' 303 | b'\x137\x137\x137' 304 | b'\x137\x137\x137' 305 | b'\x137\x137\x137' 306 | b'\x137\x137\x137' 307 | b'\x137\x137\x137' 308 | b'\x137\x137\x137' 309 | b'\x137\x137\x137' 310 | ), 311 | mock.call() 312 | .__enter__() 313 | .send( 314 | b'\xff\xff\xff\xff\xff\xff' 315 | b'\x00\x00\x00\x00\x00\x00' 316 | b'\x00\x00\x00\x00\x00\x00' 317 | b'\x00\x00\x00\x00\x00\x00' 318 | b'\x00\x00\x00\x00\x00\x00' 319 | b'\x00\x00\x00\x00\x00\x00' 320 | b'\x00\x00\x00\x00\x00\x00' 321 | b'\x00\x00\x00\x00\x00\x00' 322 | b'\x00\x00\x00\x00\x00\x00' 323 | b'\x00\x00\x00\x00\x00\x00' 324 | b'\x00\x00\x00\x00\x00\x00' 325 | b'\x00\x00\x00\x00\x00\x00' 326 | b'\x00\x00\x00\x00\x00\x00' 327 | b'\x00\x00\x00\x00\x00\x00' 328 | b'\x00\x00\x00\x00\x00\x00' 329 | b'\x00\x00\x00\x00\x00\x00' 330 | b'\x00\x00\x00\x00\x00\x00' 331 | ), 332 | mock.call().__exit__(None, None, None), 333 | ], 334 | ) 335 | 336 | @mock.patch('socket.socket') 337 | def test_send_correct_af_chosen_with_ipv6_address(self, sock: mock.Mock) -> None: 338 | """ 339 | Test whether AF_INET6 automatically chosen when the `address_family` argument is not given. 340 | """ 341 | send_magic_packet( 342 | '133713371337', 343 | '00-00-00-00-00-00', 344 | ip_address='fc00::', 345 | port=7, 346 | ) 347 | self.assertEqual( 348 | sock.mock_calls, 349 | [ 350 | mock.call(socket.AF_INET6, socket.SOCK_DGRAM), 351 | mock.call().__enter__(), 352 | mock.call() 353 | .__enter__() 354 | .setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1), 355 | mock.call().__enter__().connect(('fc00::', 7)), 356 | mock.call() 357 | .__enter__() 358 | .send( 359 | b'\xff\xff\xff\xff\xff\xff' 360 | b'\x137\x137\x137' 361 | b'\x137\x137\x137' 362 | b'\x137\x137\x137' 363 | b'\x137\x137\x137' 364 | b'\x137\x137\x137' 365 | b'\x137\x137\x137' 366 | b'\x137\x137\x137' 367 | b'\x137\x137\x137' 368 | b'\x137\x137\x137' 369 | b'\x137\x137\x137' 370 | b'\x137\x137\x137' 371 | b'\x137\x137\x137' 372 | b'\x137\x137\x137' 373 | b'\x137\x137\x137' 374 | b'\x137\x137\x137' 375 | b'\x137\x137\x137' 376 | ), 377 | mock.call() 378 | .__enter__() 379 | .send( 380 | b'\xff\xff\xff\xff\xff\xff' 381 | b'\x00\x00\x00\x00\x00\x00' 382 | b'\x00\x00\x00\x00\x00\x00' 383 | b'\x00\x00\x00\x00\x00\x00' 384 | b'\x00\x00\x00\x00\x00\x00' 385 | b'\x00\x00\x00\x00\x00\x00' 386 | b'\x00\x00\x00\x00\x00\x00' 387 | b'\x00\x00\x00\x00\x00\x00' 388 | b'\x00\x00\x00\x00\x00\x00' 389 | b'\x00\x00\x00\x00\x00\x00' 390 | b'\x00\x00\x00\x00\x00\x00' 391 | b'\x00\x00\x00\x00\x00\x00' 392 | b'\x00\x00\x00\x00\x00\x00' 393 | b'\x00\x00\x00\x00\x00\x00' 394 | b'\x00\x00\x00\x00\x00\x00' 395 | b'\x00\x00\x00\x00\x00\x00' 396 | b'\x00\x00\x00\x00\x00\x00' 397 | ), 398 | mock.call().__exit__(None, None, None), 399 | ], 400 | ) 401 | 402 | @mock.patch('socket.socket') 403 | def test_send_with_explicit_ipv6_address(self, sock: mock.Mock) -> None: 404 | """ 405 | Test whether the given address family is used instead automatically it automatically. 406 | """ 407 | send_magic_packet( 408 | '133713371337', 409 | '00-00-00-00-00-00', 410 | ip_address='example.com', 411 | port=7, 412 | address_family=socket.AF_INET6, 413 | ) 414 | self.assertEqual( 415 | sock.mock_calls, 416 | [ 417 | mock.call(socket.AF_INET6, socket.SOCK_DGRAM), 418 | mock.call().__enter__(), 419 | mock.call() 420 | .__enter__() 421 | .setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1), 422 | mock.call().__enter__().connect(('example.com', 7)), 423 | mock.call() 424 | .__enter__() 425 | .send( 426 | b'\xff\xff\xff\xff\xff\xff' 427 | b'\x137\x137\x137' 428 | b'\x137\x137\x137' 429 | b'\x137\x137\x137' 430 | b'\x137\x137\x137' 431 | b'\x137\x137\x137' 432 | b'\x137\x137\x137' 433 | b'\x137\x137\x137' 434 | b'\x137\x137\x137' 435 | b'\x137\x137\x137' 436 | b'\x137\x137\x137' 437 | b'\x137\x137\x137' 438 | b'\x137\x137\x137' 439 | b'\x137\x137\x137' 440 | b'\x137\x137\x137' 441 | b'\x137\x137\x137' 442 | b'\x137\x137\x137' 443 | ), 444 | mock.call() 445 | .__enter__() 446 | .send( 447 | b'\xff\xff\xff\xff\xff\xff' 448 | b'\x00\x00\x00\x00\x00\x00' 449 | b'\x00\x00\x00\x00\x00\x00' 450 | b'\x00\x00\x00\x00\x00\x00' 451 | b'\x00\x00\x00\x00\x00\x00' 452 | b'\x00\x00\x00\x00\x00\x00' 453 | b'\x00\x00\x00\x00\x00\x00' 454 | b'\x00\x00\x00\x00\x00\x00' 455 | b'\x00\x00\x00\x00\x00\x00' 456 | b'\x00\x00\x00\x00\x00\x00' 457 | b'\x00\x00\x00\x00\x00\x00' 458 | b'\x00\x00\x00\x00\x00\x00' 459 | b'\x00\x00\x00\x00\x00\x00' 460 | b'\x00\x00\x00\x00\x00\x00' 461 | b'\x00\x00\x00\x00\x00\x00' 462 | b'\x00\x00\x00\x00\x00\x00' 463 | b'\x00\x00\x00\x00\x00\x00' 464 | ), 465 | mock.call().__exit__(None, None, None), 466 | ], 467 | ) 468 | 469 | 470 | class TestMain(unittest.TestCase): 471 | """ 472 | Test :ref:`main`. 473 | 474 | """ 475 | 476 | @mock.patch('wakeonlan.send_magic_packet') 477 | def test_main(self, send_magic_packet: mock.Mock) -> None: 478 | """ 479 | Test if processed arguments are passed to send_magic_packet. 480 | 481 | """ 482 | main(['00:11:22:33:44:55', '-i', 'host.example', '-p', '1337']) 483 | main( 484 | [ 485 | '00:11:22:33:44:55', 486 | '-i', 487 | 'host.example', 488 | '-p', 489 | '1337', 490 | '-n', 491 | '192.168.0.2', 492 | ] 493 | ) 494 | main(['00:11:22:33:44:55', '-i', 'host.example', '-p', '1337', '-6']) 495 | self.assertEqual( 496 | send_magic_packet.mock_calls, 497 | [ 498 | mock.call( 499 | '00:11:22:33:44:55', 500 | ip_address='host.example', 501 | port=1337, 502 | interface=None, 503 | address_family=None, 504 | ), 505 | mock.call( 506 | '00:11:22:33:44:55', 507 | ip_address='host.example', 508 | port=1337, 509 | interface='192.168.0.2', 510 | address_family=None, 511 | ), 512 | mock.call( 513 | '00:11:22:33:44:55', 514 | ip_address='host.example', 515 | port=1337, 516 | interface=None, 517 | address_family=socket.AF_INET6, 518 | ), 519 | ], 520 | ) 521 | 522 | 523 | if __name__ == '__main__': 524 | unittest.main() 525 | --------------------------------------------------------------------------------