Browse Source

provide further information & workaround in exception msg when `le on` triggers `BTLEManagementError`

https://github.com/fphammerle/switchbot-mqtt/pull/31#issuecomment-916446066
Fabian Peter Hammerle 2 years ago
parent
commit
fadf3877c9
3 changed files with 109 additions and 4 deletions
  1. 4 0
      setup.py
  2. 27 4
      switchbot_mqtt/__init__.py
  3. 78 0
      tests/test_switchbot_curtain_motor_device_info.py

+ 4 - 0
setup.py

@@ -72,6 +72,10 @@ setuptools.setup(
     ],
     entry_points={"console_scripts": ["switchbot-mqtt = switchbot_mqtt:_main"]},
     install_requires=[
+        # >=1.3.0 for btle.BTLEManagementError (could be replaced with BTLEException)
+        # >=0.1.0 for btle.helperExe
+        # https://github.com/IanHarvey/bluepy/tree/v/1.3.0#release-notes
+        "bluepy>=1.3.0,<2",
         # >=0.10.0 for SwitchbotCurtain.{update,get_position}
         "PySwitchbot>=0.10.0,<0.11",
         "paho-mqtt<2",

+ 27 - 4
switchbot_mqtt/__init__.py

@@ -24,8 +24,10 @@ import json
 import logging
 import pathlib
 import re
+import shlex
 import typing
 
+import bluepy.btle
 import paho.mqtt.client
 import switchbot
 
@@ -279,10 +281,31 @@ class _CurtainMotor(_MQTTControlledActor):
         )
 
     def _update_position(self, mqtt_client: paho.mqtt.client.Client) -> None:
-        # Requires running bluepy-helper executable with CAP_NET_ADMIN
-        # https://github.com/IanHarvey/bluepy/issues/313#issuecomment-428324639
-        # https://github.com/fphammerle/switchbot-mqtt/pull/31#issuecomment-840704962
-        self._device.update()
+        try:
+            self._device.update()
+        except bluepy.btle.BTLEManagementError as exc:
+            if (
+                exc.emsg == "Permission Denied"
+                and exc.message == "Failed to execute management command 'le on'"
+            ):
+                raise PermissionError(
+                    "bluepy-helper failed to enable low energy mode"
+                    + " due to insufficient permissions."
+                    + "\nSee {}, {}, and {}.".format(
+                        "https://github.com/IanHarvey/bluepy/issues/313#issuecomment-428324639",
+                        "https://github.com/fphammerle/switchbot-mqtt/pull/31"
+                        + "#issuecomment-846383603",
+                        "https://github.com/IanHarvey/bluepy/blob/v/1.3.0/bluepy/bluepy-helper.c"
+                        + "#L1260",
+                    )
+                    + "\nInsecure workaround:"
+                    + "\n1. sudo apt-get install --no-install-recommends libcap2-bin"
+                    + "\n2. sudo setcap cap_net_admin+ep {}".format(
+                        shlex.quote(bluepy.btle.helperExe)
+                    )
+                    + "\n3. restart switchbot-mqtt"
+                ) from exc
+            raise
         self._report_position(mqtt_client=mqtt_client)
 
     def execute_command(

+ 78 - 0
tests/test_switchbot_curtain_motor_device_info.py

@@ -0,0 +1,78 @@
+# switchbot-mqtt - MQTT client controlling SwitchBot button & curtain automators,
+# compatible with home-assistant.io's MQTT Switch & Cover platform
+#
+# Copyright (C) 2021 Fabian Peter Hammerle <fabian@hammerle.me>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+import os
+import re
+import unittest.mock
+
+import bluepy.btle
+import pytest
+
+import switchbot_mqtt
+
+# pylint: disable=protected-access
+
+
+def test__update_position_le_on_permission_denied():
+    actor = switchbot_mqtt._CurtainMotor(
+        mac_address="dummy", retry_count=21, password=None
+    )
+    with unittest.mock.patch(
+        "switchbot.SwitchbotCurtain.update",
+        side_effect=bluepy.btle.BTLEManagementError(
+            "Failed to execute management command 'le on'",
+            {
+                "rsp": ["mgmt"],
+                "code": ["mgmterr"],
+                "estat": [20],
+                "emsg": ["Permission Denied"],
+            },
+        ),
+    ) as update_mock, unittest.mock.patch.object(
+        actor, "_report_position"
+    ) as report_position_mock, pytest.raises(
+        PermissionError
+    ) as exc_info:
+        actor._update_position(mqtt_client="client")
+    update_mock.assert_called_once_with()
+    report_position_mock.assert_not_called()
+    assert os.path.isfile(
+        re.search(
+            r"sudo setcap cap_net_admin\+ep (\S+/bluepy-helper)\b",
+            exc_info.exconly(),
+        ).group(1)
+    )
+    assert isinstance(exc_info.value.__cause__, bluepy.btle.BTLEManagementError)
+
+
+def test__update_position_other_error():
+    actor = switchbot_mqtt._CurtainMotor(
+        mac_address="dummy", retry_count=21, password=None
+    )
+    side_effect = bluepy.btle.BTLEManagementError("test")
+    with unittest.mock.patch(
+        "switchbot.SwitchbotCurtain.update", side_effect=side_effect
+    ) as update_mock, unittest.mock.patch.object(
+        actor, "_report_position"
+    ) as report_position_mock, pytest.raises(
+        type(side_effect)
+    ) as exc_info:
+        actor._update_position(mqtt_client="client")
+    update_mock.assert_called_once_with()
+    report_position_mock.assert_not_called()
+    assert exc_info.value == side_effect