From e3074864e341f61f4da044591e6d5772ceaec7f7 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 26 Mar 2018 22:41:25 +0200 Subject: [PATCH] Support for the Xiaomi Mi WiFi Repeater 2 added (#278) (Closes: #275) --- miio/__init__.py | 1 + miio/tests/test_wifirepeater.py | 145 ++++++++++++++++++++++++++++++++ miio/wifirepeater.py | 97 +++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 miio/tests/test_wifirepeater.py create mode 100644 miio/wifirepeater.py diff --git a/miio/__init__.py b/miio/__init__.py index 6b8000d75..e97c9e430 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -14,6 +14,7 @@ from miio.chuangmi_ir import ChuangmiIr from miio.fan import Fan from miio.wifispeaker import WifiSpeaker +from miio.wifirepeater import WifiRepeater from miio.airqualitymonitor import AirQualityMonitor from miio.airconditioningcompanion import AirConditioningCompanion from miio.yeelight import Yeelight diff --git a/miio/tests/test_wifirepeater.py b/miio/tests/test_wifirepeater.py new file mode 100644 index 000000000..9097839f0 --- /dev/null +++ b/miio/tests/test_wifirepeater.py @@ -0,0 +1,145 @@ +from unittest import TestCase +from miio import WifiRepeater +from miio.wifirepeater import WifiRepeaterConfiguration, WifiRepeaterStatus +import pytest + + +class DummyWifiRepeater(WifiRepeater): + def __init__(self, *args, **kwargs): + self.state = { + 'sta': {'count': 2, 'access_policy': 0}, + 'mat': [ + {'mac': 'aa:aa:aa:aa:aa:aa', 'ip': '192.168.1.133', + 'last_time': 54371873}, + {'mac': 'bb:bb:bb:bb:bb:bb', 'ip': '192.168.1.156', + 'last_time': 54371496} + ], + 'access_list': {'mac': ''} + } + self.config = {'ssid': 'SSID', 'pwd': 'PWD', 'hidden': 0} + self.device_info = { + 'life': 543452, 'cfg_time': 543452, + 'token': 'ffffffffffffffffffffffffffffffff', + 'fw_ver': '2.2.14', 'hw_ver': 'R02', + 'uid': 1583412143, 'api_level': 2, + 'mcu_fw_ver': '1000', 'wifi_fw_ver': '1.0.0', + 'mac': 'FF:FF:FF:FF:FF:FF', + 'model': 'xiaomi.repeater.v2', + 'ap': {'rssi': -63, 'ssid': 'SSID', + 'bssid': 'EE:EE:EE:EE:EE:EE', + 'rx': 136695922, 'tx': 1779521233}, + 'sta': {'count': 2, 'ssid': 'REPEATER-SSID', + 'hidden': 0, + 'assoclist': 'cc:cc:cc:cc:cc:cc;bb:bb:bb:bb:bb:bb;'}, + 'netif': {'localIp': '192.168.1.170', + 'mask': '255.255.255.0', + 'gw': '192.168.1.1'}, + 'desc': {'wifi_explorer': 1, + 'sn': '14923 / 20191356', 'color': 101, + 'channel': 'release'} + } + + self.return_values = { + 'miIO.get_repeater_sta_info': self._get_state, + 'miIO.get_repeater_ap_info': self._get_configuration, + 'miIO.switch_wifi_explorer': self._set_wifi_explorer, + 'miIO.switch_wifi_ssid': self._set_configuration, + 'miIO.info': self._get_info, + } + self.start_state = self.state.copy() + self.start_config = self.config.copy() + self.start_device_info = self.device_info.copy() + + def send(self, command: str, parameters=None, retry_count=3): + """Overridden send() to return values from `self.return_values`.""" + return self.return_values[command](parameters) + + def _reset_state(self): + """Revert back to the original state.""" + self.state = self.start_state.copy() + self.config = self.start_config.copy() + self.device_info = self.start_device_info.copy() + + def _get_state(self, param): + return self.state + + def _get_configuration(self, param): + return self.config + + def _get_info(self, param): + return self.device_info + + def _set_wifi_explorer(self, data): + self.device_info['desc']['wifi_explorer'] = data[0]['wifi_explorer'] + + def _set_configuration(self, data): + self.config = { + 'ssid': data[0]['ssid'], + 'pwd': data[0]['pwd'], + 'hidden': data[0]['hidden'] + } + + self.device_info['desc']['wifi_explorer'] = data[0]['wifi_explorer'] + return True + + +@pytest.fixture(scope="class") +def wifirepeater(request): + request.cls.device = DummyWifiRepeater() + # TODO add ability to test on a real device + + +@pytest.mark.usefixtures("wifirepeater") +class TestWifiRepeater(TestCase): + def state(self): + return self.device.status() + + def configuration(self): + return self.device.configuration() + + def info(self): + return self.device.info() + + def test_status(self): + self.device._reset_state() + + assert repr(self.state()) == repr(WifiRepeaterStatus(self.device.start_state)) + + assert self.state().access_policy == self.device.start_state['sta']['access_policy'] + assert self.state().associated_stations == self.device.start_state['mat'] + + def test_set_wifi_roaming(self): + self.device.set_wifi_roaming(True) + assert self.info().raw['desc']['wifi_explorer'] == 1 + + self.device.set_wifi_roaming(False) + assert self.info().raw['desc']['wifi_explorer'] == 0 + + def test_configuration(self): + self.device._reset_state() + + assert repr(self.configuration()) == repr(WifiRepeaterConfiguration(self.device.start_config)) + + assert self.configuration().ssid == self.device.start_config['ssid'] + assert self.configuration().password == self.device.start_config['pwd'] + assert self.configuration().ssid_hidden is (self.device.start_config['hidden'] == 1) + + def test_set_configuration(self): + def configuration(): + return self.device.configuration() + + dummy_configuration = { + 'ssid': 'SSID2', + 'password': 'PASSWORD2', + 'hidden': True, + 'wifi_explorer': False + } + + self.device.set_configuration( + dummy_configuration['ssid'], + dummy_configuration['password'], + dummy_configuration['hidden'], + dummy_configuration['wifi_explorer']) + assert configuration().ssid == dummy_configuration['ssid'] + assert configuration().password == dummy_configuration['password'] + assert configuration().ssid_hidden is dummy_configuration['hidden'] diff --git a/miio/wifirepeater.py b/miio/wifirepeater.py new file mode 100644 index 000000000..3be4665c1 --- /dev/null +++ b/miio/wifirepeater.py @@ -0,0 +1,97 @@ +import logging +from .device import Device + +_LOGGER = logging.getLogger(__name__) + + +class WifiRepeaterStatus: + def __init__(self, data): + """ + Response of a xiaomi.repeater.v2: + + { + 'sta': {'count': 2, 'access_policy': 0}, + 'mat': [ + {'mac': 'aa:aa:aa:aa:aa:aa', 'ip': '192.168.1.133', 'last_time': 54371873}, + {'mac': 'bb:bb:bb:bb:bb:bb', 'ip': '192.168.1.156', 'last_time': 54371496} + ], + 'access_list': {'mac': ''} + } + """ + self.data = data + + @property + def access_policy(self) -> int: + """Access policy of the associated stations.""" + return self.data['sta']['access_policy'] + + @property + def associated_stations(self) -> dict: + """List of associated stations.""" + return self.data['mat'] + + def __repr__(self) -> str: + s = "" % \ + (self.access_policy, + len(self.associated_stations)) + return s + + +class WifiRepeaterConfiguration: + def __init__(self, data): + """ + Response of a xiaomi.repeater.v2: + + {'ssid': 'SSID', 'pwd': 'PWD', 'hidden': 0} + """ + self.data = data + + @property + def ssid(self) -> str: + return self.data['ssid'] + + @property + def password(self) -> str: + return self.data['pwd'] + + @property + def ssid_hidden(self) -> bool: + return self.data['hidden'] == 1 + + def __repr__(self) -> str: + s = "" % \ + (self.ssid, + self.password, + self.ssid_hidden) + return s + + +class WifiRepeater(Device): + """Device class for Xiaomi Mi WiFi Repeater 2.""" + def status(self) -> WifiRepeaterStatus: + """Return the associated stations.""" + return WifiRepeaterStatus(self.send("miIO.get_repeater_sta_info", [])) + + def configuration(self) -> WifiRepeaterConfiguration: + """Return the configuration of the accesspoint.""" + return WifiRepeaterConfiguration( + self.send("miIO.get_repeater_ap_info", [])) + + def set_wifi_roaming(self, wifi_roaming: bool): + """Turn the WiFi roaming on/off.""" + return self.send("miIO.switch_wifi_explorer", [{ + 'wifi_explorer': int(wifi_roaming) + }]) + + def set_configuration(self, ssid: str, password: str, hidden: bool = False, + wifi_roaming: bool = False): + """Update the configuration of the accesspoint.""" + return self.send("miIO.switch_wifi_ssid", [{ + 'ssid': ssid, + 'pwd': password, + 'hidden': int(hidden), + 'wifi_explorer': int(wifi_roaming) + }])