From edfaec6f3ca7504aa3ea2cdcc8b28a1a67f2778c Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 13 Apr 2023 15:14:01 +0200 Subject: [PATCH] Add multiap feature (#123) --- devolo_plc_api/device_api/__init__.py | 2 + devolo_plc_api/device_api/deviceapi.py | 14 ++++++ devolo_plc_api/device_api/deviceapi.pyi | 3 ++ devolo_plc_api/device_api/multiap_pb2.py | 26 ++++++++++ devolo_plc_api/device_api/multiap_pb2.pyi | 59 +++++++++++++++++++++++ docs/CHANGELOG.md | 6 ++- example_async.py | 6 +++ example_sync.py | 6 +++ tests/test_data.json | 2 +- tests/test_device.py | 2 +- tests/test_deviceapi.py | 18 +++++++ 11 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 devolo_plc_api/device_api/multiap_pb2.py create mode 100644 devolo_plc_api/device_api/multiap_pb2.pyi diff --git a/devolo_plc_api/device_api/__init__.py b/devolo_plc_api/device_api/__init__.py index 171a9af..b0c9554 100644 --- a/devolo_plc_api/device_api/__init__.py +++ b/devolo_plc_api/device_api/__init__.py @@ -2,6 +2,7 @@ import re from .deviceapi import DeviceApi +from .multiap_pb2 import WifiMultiApGetResponse from .support_pb2 import SupportInfoDump from .updatefirmware_pb2 import UpdateFirmwareCheck from .wifinetwork_pb2 import ( @@ -33,6 +34,7 @@ "RepeatedAPInfo", "SupportInfoItem", "WifiGuestAccessGet", + "WifiMultiApGetResponse", "CONFIGLAYER_FORMAT", "SERVICE_TYPE", "UPDATE_AVAILABLE", diff --git a/devolo_plc_api/device_api/deviceapi.py b/devolo_plc_api/device_api/deviceapi.py index 07afedd..aa2011d 100644 --- a/devolo_plc_api/device_api/deviceapi.py +++ b/devolo_plc_api/device_api/deviceapi.py @@ -12,6 +12,7 @@ from .factoryreset_pb2 import FactoryResetStart from .ledsettings_pb2 import LedSettingsGet, LedSettingsSet, LedSettingsSetResponse +from .multiap_pb2 import WifiMultiApGetResponse from .restart_pb2 import RestartResponse, UptimeGetResponse from .support_pb2 import SupportInfoDump, SupportInfoDumpResponse from .updatefirmware_pb2 import UpdateFirmwareCheck, UpdateFirmwareStart @@ -105,6 +106,19 @@ async def async_set_led_setting(self, enable: bool) -> bool: response.ParseFromString(await query.aread()) return response.result == response.SUCCESS + @_feature("multiap") + async def async_get_wifi_multi_ap(self) -> WifiMultiApGetResponse: + """ + Get MultiAP details asynchronously. This feature only works on devices, that announce the multiap feature. + + return: MultiAP details + """ + self._logger.debug("Getting MultiAP details.") + query = await self._async_get("WifiMultiApGet") + response = WifiMultiApGetResponse() + response.ParseFromString(await query.aread()) + return response + @_feature("repeater0") async def async_get_wifi_repeated_access_points(self) -> list[WifiRepeatedAPsGet.RepeatedAPInfo]: """ diff --git a/devolo_plc_api/device_api/deviceapi.pyi b/devolo_plc_api/device_api/deviceapi.pyi index 7293a93..5321c99 100644 --- a/devolo_plc_api/device_api/deviceapi.pyi +++ b/devolo_plc_api/device_api/deviceapi.pyi @@ -3,6 +3,7 @@ isort:skip_file """ from __future__ import annotations +from .multiap_pb2 import WifiMultiApGetResponse from .support_pb2 import SupportInfoDump from .updatefirmware_pb2 import UpdateFirmwareCheck from .wifinetwork_pb2 import WifiConnectedStationsGet, WifiGuestAccessGet, WifiNeighborAPsGet, WifiRepeatedAPsGet @@ -16,6 +17,7 @@ class DeviceApi(Protobuf): def __init__(self, ip: str, session: AsyncClient, info: ZeroconfServiceInfo) -> None: ... async def async_get_led_setting(self) -> bool: ... async def async_set_led_setting(self, enable: bool) -> bool: ... + async def async_get_wifi_multi_ap(self) -> WifiMultiApGetResponse: ... async def async_get_wifi_repeated_access_points(self) -> list[WifiRepeatedAPsGet.RepeatedAPInfo]: ... async def async_start_wps_clone(self) -> bool: ... async def async_factory_reset(self) -> bool: ... @@ -31,6 +33,7 @@ class DeviceApi(Protobuf): async def async_start_wps(self) -> bool: ... def get_led_setting(self) -> bool: ... def set_led_setting(self, enable: bool) -> bool: ... + def get_wifi_multi_ap(self) -> WifiMultiApGetResponse: ... def get_wifi_repeated_access_points(self) -> list[WifiRepeatedAPsGet.RepeatedAPInfo]: ... def start_wps_clone(self) -> bool: ... def factory_reset(self) -> bool: ... diff --git a/devolo_plc_api/device_api/multiap_pb2.py b/devolo_plc_api/device_api/multiap_pb2.py new file mode 100644 index 0000000..5608f18 --- /dev/null +++ b/devolo_plc_api/device_api/multiap_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: multiap.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rmultiap.proto\x12\ndevice.api\"W\n\x16WifiMultiApGetResponse\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x15\n\rcontroller_id\x18\x02 \x01(\t\x12\x15\n\rcontroller_ip\x18\x03 \x01(\tB\x11\n\x06\x64\x65viceB\x07Multiapb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'multiap_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\006deviceB\007Multiap' + _WIFIMULTIAPGETRESPONSE._serialized_start=29 + _WIFIMULTIAPGETRESPONSE._serialized_end=116 +# @@protoc_insertion_point(module_scope) diff --git a/devolo_plc_api/device_api/multiap_pb2.pyi b/devolo_plc_api/device_api/multiap_pb2.pyi new file mode 100644 index 0000000..414dd3c --- /dev/null +++ b/devolo_plc_api/device_api/multiap_pb2.pyi @@ -0,0 +1,59 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class WifiMultiApGetResponse(google.protobuf.message.Message): + """Details about MultiAP as returned by the 'WifiMultiApGet' endpoint.""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ENABLED_FIELD_NUMBER: builtins.int + CONTROLLER_ID_FIELD_NUMBER: builtins.int + CONTROLLER_IP_FIELD_NUMBER: builtins.int + enabled: builtins.bool + """Describes if the MultiAP functionality is enabled in the device.""" + controller_id: builtins.str + """The id of the mesh controller, in form of its MAC address, + if a mesh controller is known to the device. + If the device is not aware of a mesh controller, e.g. because + none has been elected yet, it is left empty. + + The MAC address is represented as a string of 12 hexadecimal + digits (digits 0-9, letters A-F or a-f) displayed as six pairs of + digits separated by colons. + """ + controller_ip: builtins.str + """The IP address of the known mesh controller, if the implementation + provides it. + If the device is not aware of a mesh controller or doesn't + know its IP, it is left empty. + + The IP can be an IPv4 in dot-separated decimal format, or an IPv6 + in colon-separated hexadecimal format. In case multiple IPs are + known, the value can be a comma-separated string of either formats. + Also, an IP can optionally be prefixed with an identifier separated + from the IP with a semicolon. + """ + def __init__( + self, + *, + enabled: builtins.bool = ..., + controller_id: builtins.str = ..., + controller_ip: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["controller_id", b"controller_id", "controller_ip", b"controller_ip", "enabled", b"enabled"]) -> None: ... + +global___WifiMultiApGetResponse = WifiMultiApGetResponse diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b49945e..34860ac 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [v1.3.0] - 2023/04/13 + +### Added + +- Get MultiAP information from the device ### Fixed diff --git a/example_async.py b/example_async.py index 944f677..fa24083 100644 --- a/example_async.py +++ b/example_async.py @@ -22,6 +22,12 @@ async def run(): # If the state was changed successfully, True is returned, otherwise False. print("success" if await dpa.device.async_set_led_setting(enable=True) else "failed") + # Get MultiAP details. If the device is not aware of a mesh controller or doesn't know its IP, it is left empty. + multi_ap = await dpa.device.async_get_wifi_multi_ap() + print(multi_ap.enabled) # True + print(multi_ap.controller_id) # "AA:BB:CC:DD:EE:FF" + print(multi_ap.controller_ip) # "192.0.2.1" + # Factory reset the device. If the reset will happen shortly, True is returned, otherwise False. print("success" if await dpa.device.async_factory_reset() else "failed") diff --git a/example_sync.py b/example_sync.py index 0770a71..8f5f9bb 100644 --- a/example_sync.py +++ b/example_sync.py @@ -20,6 +20,12 @@ def run(): # If the state was changed successfully, True is returned, otherwise False. print("success" if dpa.device.set_led_setting(enable=True) else "failed") + # Get MultiAP details. If the device is not aware of a mesh controller or doesn't know its IP, it is left empty. + multi_ap = dpa.device.get_wifi_multi_ap() + print(multi_ap.enabled) # True + print(multi_ap.controller_id) # "AA:BB:CC:DD:EE:FF" + print(multi_ap.controller_ip) # "192.0.2.1" + # Factory reset the device. If the reset will happen shortly, True is returned, otherwise False. print("success" if dpa.device.factory_reset() else "failed") diff --git a/tests/test_data.json b/tests/test_data.json index 8877d3b..c792a8f 100644 --- a/tests/test_data.json +++ b/tests/test_data.json @@ -24,5 +24,5 @@ } }, "hostname": "device.local", - "ip": "192.168.0.10" + "ip": "192.0.2.1" } diff --git a/tests/test_device.py b/tests/test_device.py index 73fc858..ded9adf 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -205,7 +205,7 @@ async def test__get_service_info_alien(self, mock_info_from_service: Mock): with patch("devolo_plc_api.device.AsyncServiceInfo", StubAsyncServiceInfo), patch( "devolo_plc_api.device.PlcNetApi" ), pytest.raises(DeviceNotFound): - mock_device = Device(ip="192.168.0.11") + mock_device = Device(ip="192.0.2.2") await mock_device.async_connect() assert StubAsyncServiceInfo.async_request.call_count == 1 assert mock_info_from_service.call_count == 0 diff --git a/tests/test_deviceapi.py b/tests/test_deviceapi.py index ce1e916..6224181 100644 --- a/tests/test_deviceapi.py +++ b/tests/test_deviceapi.py @@ -7,6 +7,7 @@ from devolo_plc_api.device_api import ConnectedStationInfo, DeviceApi, NeighborAPInfo, RepeatedAPInfo, SupportInfoItem from devolo_plc_api.device_api.factoryreset_pb2 import FactoryResetStart from devolo_plc_api.device_api.ledsettings_pb2 import LedSettingsGet, LedSettingsSetResponse +from devolo_plc_api.device_api.multiap_pb2 import WifiMultiApGetResponse from devolo_plc_api.device_api.restart_pb2 import RestartResponse, UptimeGetResponse from devolo_plc_api.device_api.support_pb2 import SupportInfoDump, SupportInfoDumpResponse from devolo_plc_api.device_api.updatefirmware_pb2 import UpdateFirmwareCheck, UpdateFirmwareStart @@ -67,6 +68,23 @@ def test_set_led_setting(self, device_api: DeviceApi, httpx_mock: HTTPXMock): httpx_mock.add_response(content=led_setting_set.SerializeToString()) assert device_api.set_led_setting(True) + @pytest.mark.asyncio() + @pytest.mark.parametrize("feature", ["multiap"]) + async def test_async_get_wifi_multi_ap(self, device_api: DeviceApi, httpx_mock: HTTPXMock): + """Test setting LED settings asynchronously.""" + multi_ap_details = WifiMultiApGetResponse(enabled=True) + httpx_mock.add_response(content=multi_ap_details.SerializeToString()) + details = await device_api.async_get_wifi_multi_ap() + assert details.enabled + + @pytest.mark.parametrize("feature", ["multiap"]) + def test_get_wifi_multi_ap(self, device_api: DeviceApi, httpx_mock: HTTPXMock): + """Test setting LED settings synchronously.""" + multi_ap_details = WifiMultiApGetResponse(enabled=True) + httpx_mock.add_response(content=multi_ap_details.SerializeToString()) + details = device_api.get_wifi_multi_ap() + assert details.enabled + @pytest.mark.asyncio() @pytest.mark.parametrize("feature", ["repeater0"]) async def test_async_get_wifi_repeated_access_points(