From 8d44343b7e33187368e75dc9bcd0b540d512ab16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Br=C3=A4ucker?= <7303810+chrisbraucker@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:07:24 +0200 Subject: [PATCH 1/2] Add wake on lan API method to fritzhosts --- fritzconnection/lib/fritzhosts.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fritzconnection/lib/fritzhosts.py b/fritzconnection/lib/fritzhosts.py index a7ea9cd..d4f5c0c 100644 --- a/fritzconnection/lib/fritzhosts.py +++ b/fritzconnection/lib/fritzhosts.py @@ -169,6 +169,13 @@ def set_wakeonlan_status(self, mac_address: str, status: bool = False) -> None: } self._action("X_AVM-DE_SetAutoWakeOnLANByMACAddress", arguments=args) + def wakeonlan_host(self, mac_address: str) -> None: + """ + Triggers sending a wake on lan message with the given `mac_address` + on the local network. This method has no return value. + """ + self._action("X_AVM-DE_WakeOnLANByMACAddress", NewMACAddress=mac_address) + def set_host_name(self, mac_address: str, name: str) -> None: """ Sets the hostname of the device with the given `mac_address` to From 8af730c1f85b0f605adacbe376ca9012086722af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Br=C3=A4ucker?= <7303810+chrisbraucker@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:24:43 +0200 Subject: [PATCH 2/2] Add fritzwol CLI interface and tests --- fritzconnection/cli/fritzwol.py | 96 ++++++++++++++++++++++ fritzconnection/tests/cli/test_fritzwol.py | 59 +++++++++++++ setup.py | 1 + 3 files changed, 156 insertions(+) create mode 100644 fritzconnection/cli/fritzwol.py create mode 100644 fritzconnection/tests/cli/test_fritzwol.py diff --git a/fritzconnection/cli/fritzwol.py b/fritzconnection/cli/fritzwol.py new file mode 100644 index 0000000..d8e94cd --- /dev/null +++ b/fritzconnection/cli/fritzwol.py @@ -0,0 +1,96 @@ +""" +fritzwol.py + +Module to wake up a single host via the Fritzbox built-in mechanism. +This can be helpful if the host to be woken up is in a different +broadcast domain/ subnet than the client which tries to wake up. +CLI interface. + +This module is part of the FritzConnection package. +https://github.com/kbr/fritzconnection +License: MIT (https://opensource.org/licenses/MIT) +Authors: Maik Töpfer, Chris Bräucker +""" + +from fritzconnection.core.exceptions import FritzArgumentError, FritzArrayIndexError, FritzAuthorizationError, FritzLookUpError +from ..lib.fritzhosts import FritzHosts +from . utils import ( + get_cli_arguments, + get_instance, + print_header, + print_common_exception_message +) + +class DeviceUnknownException(Exception): + """Exception raised if the reference to the host does not resolve.""" + + +def wake_host(fh, args): + """ + Either wakes a host directly by MAC address, which should even work for hosts not known. + Or it tries to find the given parameter in the device list to determine the MAC address. + """ + mac = args.host + + if args.field == 'n': + try: + host = fh.get_generic_host_entry(int(args.host) - 1) + except (FritzArgumentError, FritzArrayIndexError) as err: + raise DeviceUnknownException("The index provided is invalid", args.host) + mac = host['NewMACAddress'] + + elif args.field == 'ip': + try: + host = fh.get_specific_host_entry_by_ip(args.host) + except (FritzArgumentError, FritzLookUpError) as err: + raise DeviceUnknownException("The IP provided is unknown", args.host) + mac = host['NewMACAddress'] + + elif args.field == 'name': + found = False + for entry in fh.get_generic_host_entries(): + if entry['NewHostName'].lower() == args.host.lower(): + mac = entry['NewMACAddress'] + found = True + break + + if not found: + raise DeviceUnknownException("The hostname provided is unknown", args.host) + + fh.wakeonlan_host(mac) + print(f"Waking {mac}") + + + +def add_arguments(parser): + parser.add_argument('field', + choices=('ip', 'name', 'mac', 'n'), + default='mac', + nargs='?', + help='Which host field to wake by. ' + + 'Retrieve this data with the `fritzhosts` command. ' + + '\'mac\' sends the WoL package directly, without checking. ' + + '(default: mac)') + parser.add_argument('host', help='Field value of host to be woken up') + + +def execute(): + arguments = get_cli_arguments(add_arguments) + fh = get_instance(FritzHosts, arguments) + print_header(fh) + wake_host(fh, arguments) + + +def main(): + try: + execute() + except FritzAuthorizationError as err: + print_common_exception_message(err) + except FritzArgumentError: + print(f"Error: Invalid MAC address format") + except DeviceUnknownException as err: + print(f"Error: {err.args[0]}: {err.args[1]}") + + +if __name__ == '__main__': + main() diff --git a/fritzconnection/tests/cli/test_fritzwol.py b/fritzconnection/tests/cli/test_fritzwol.py new file mode 100644 index 0000000..147fcbb --- /dev/null +++ b/fritzconnection/tests/cli/test_fritzwol.py @@ -0,0 +1,59 @@ +from unittest.mock import Mock +from argparse import Namespace + +from fritzconnection.cli.fritzwol import wake_host + + +def test_calls_wakeonlan_host_with_macaddress_directly(): + mac = 'C0:FF:EE:C0:FF:EE' + fritz_host = Mock() + + wake_host(fritz_host, Namespace(field='mac', host=mac)) + fritz_host.get_generic_host_entry.assert_not_called() + fritz_host.get_specific_host_entry_by_ip.assert_not_called() + fritz_host.get_generic_host_entries.assert_not_called() + + fritz_host.wakeonlan_host.assert_called_with(mac) + + +def test_n_calls_generic_host_then_wakeonlan(): + mac = 'C0:FF:EE:C0:FF:EE' + fritz_host = Mock() + fritz_host.get_generic_host_entry.return_value = {'NewMACAddress': mac} + + wake_host(fritz_host, Namespace(field='n', host='1')) + fritz_host.get_generic_host_entry.assert_called_with(0) + fritz_host.get_specific_host_entry_by_ip.assert_not_called() + fritz_host.get_generic_host_entries.assert_not_called() + + fritz_host.wakeonlan_host.assert_called_with(mac) + + +def test_ip_calls_specific_host_then_wakeonlan(): + mac = 'C0:FF:EE:C0:FF:EE' + fritz_host = Mock() + fritz_host.get_specific_host_entry_by_ip.return_value = {'NewMACAddress': mac} + + wake_host(fritz_host, Namespace(field='ip', host='127.0.0.1')) + fritz_host.get_generic_host_entry.assert_not_called() + fritz_host.get_specific_host_entry_by_ip.assert_called_with('127.0.0.1') + fritz_host.get_generic_host_entries.assert_not_called() + + fritz_host.wakeonlan_host.assert_called_with(mac) + + + +def test_name_calls_generic_host_entries_then_wakeonlan(): + mac = 'C0:FF:EE:C0:FF:EE' + fritz_host = Mock() + fritz_host.get_generic_host_entries.return_value = [ + {'NewHostName': 'otherhost', 'NewMACAddress': '11:22:33:44:55:66'}, + {'NewHostName': 'thishost', 'NewMACAddress': mac} + ] + + wake_host(fritz_host, Namespace(field='name', host='thishost')) + fritz_host.get_generic_host_entry.assert_not_called() + fritz_host.get_specific_host_entry_by_ip.assert_not_called() + fritz_host.get_generic_host_entries.assert_called_once() + + fritz_host.wakeonlan_host.assert_called_with(mac) diff --git a/setup.py b/setup.py index 1a8ed4b..5048ea1 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ def get_version(): "fritzphonebook = fritzconnection.cli.fritzphonebook:main", "fritzstatus = fritzconnection.cli.fritzstatus:main", "fritzwlan = fritzconnection.cli.fritzwlan:main", + "fritzwol = fritzconnection.cli.fritzwol:main", ] }, )