Browse Source

added command-line param `--device-password-file`

https://github.com/fphammerle/switchbot-mqtt/issues/37#issue-930973360
Fabian Peter Hammerle 2 years ago
parent
commit
5dc3fc3b75
6 changed files with 94 additions and 8 deletions
  1. 4 0
      .pylintrc
  2. 5 1
      CHANGELOG.md
  3. 16 0
      README.md
  4. 18 1
      switchbot_mqtt/__init__.py
  5. 41 4
      tests/test_cli.py
  6. 10 2
      tests/test_mqtt.py

+ 4 - 0
.pylintrc

@@ -3,3 +3,7 @@
 disable=bad-continuation, # black
         missing-function-docstring,
         missing-module-docstring
+
+[DESIGN]
+
+max-args=8

+ 5 - 1
CHANGELOG.md

@@ -5,10 +5,14 @@ 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]
+### Added
+- optional command-line parameter `--device-password-file`
+  providing path to JSON file mapping mac addresses to the password
+  that is required to control the respective switchbot device.
 
 ## [0.7.0] - 2021-07-09
 ### Added
-- Command-line parameter `--retries` to alter maximum number of attempts to send a command
+- command-line parameter `--retries` to alter maximum number of attempts to send a command
   to a SwitchBot device (default unchanged)
 
 ### Fixed

+ 16 - 0
README.md

@@ -46,6 +46,22 @@ Send `OPEN`, `CLOSE`, or `STOP` to topic `homeassistant/cover/switchbot-curtain/
 $ mosquitto_pub -h MQTT_BROKER -t homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/set -m CLOSE
 ```
 
+### Device Passwords
+
+In case some of your Switchbot devices are password-protected,
+create a JSON file mapping MAC addresses to passwords
+and provide its path via the `--device-password-file` option:
+```json
+{
+  "11:22:33:44:55:66": "password",
+  "aa:bb:cc:dd:ee:ff": "secret",
+  "00:00:00:0f:f1:ce": "random string"
+}
+```
+```sh
+$ switchbot-mqtt --device-password-file /some/where/switchbot-passwords.json …
+```
+
 ## Home Assistant 🏡
 
 ### Rationale

+ 18 - 1
switchbot_mqtt/__init__.py

@@ -20,6 +20,7 @@ import abc
 import argparse
 import collections
 import enum
+import json
 import logging
 import pathlib
 import re
@@ -293,10 +294,13 @@ def _run(
     mqtt_username: typing.Optional[str],
     mqtt_password: typing.Optional[str],
     retry_count: int,
+    device_passwords: typing.Dict[str, str],
 ) -> None:
     # https://pypi.org/project/paho-mqtt/
     mqtt_client = paho.mqtt.client.Client(
-        userdata=_MQTTCallbackUserdata(retry_count=retry_count, device_passwords={})
+        userdata=_MQTTCallbackUserdata(
+            retry_count=retry_count, device_passwords=device_passwords
+        )
     )
     mqtt_client.on_connect = _mqtt_on_connect
     _LOGGER.info("connecting to MQTT broker %s:%d", mqtt_host, mqtt_port)
@@ -331,6 +335,14 @@ def _main() -> None:
         dest="mqtt_password_path",
         help="stripping trailing newline",
     )
+    argparser.add_argument(
+        "--device-password-file",
+        type=pathlib.Path,
+        metavar="PATH",
+        dest="device_password_path",
+        help="path to json file mapping mac addresses of switchbot devices to passwords, e.g. "
+        + json.dumps({"11:22:33:44:55:66": "password", "aa:bb:cc:dd:ee:ff": "secret"}),
+    )
     argparser.add_argument(
         "--retries",
         dest="retry_count",
@@ -349,10 +361,15 @@ def _main() -> None:
             mqtt_password = mqtt_password[:-1]
     else:
         mqtt_password = args.mqtt_password
+    if args.device_password_path:
+        device_passwords = json.loads(args.device_password_path.read_text())
+    else:
+        device_passwords = {}
     _run(
         mqtt_host=args.mqtt_host,
         mqtt_port=args.mqtt_port,
         mqtt_username=args.mqtt_username,
         mqtt_password=mqtt_password,
         retry_count=args.retry_count,
+        device_passwords=device_passwords,
     )

+ 41 - 4
tests/test_cli.py

@@ -16,6 +16,7 @@
 # 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 json
 import unittest.mock
 
 import pytest
@@ -98,11 +99,12 @@ def test__main(
         mqtt_username=expected_username,
         mqtt_password=expected_password,
         retry_count=expected_retry_count,
+        device_passwords={},
     )
 
 
 @pytest.mark.parametrize(
-    ("password_file_content", "expected_password"),
+    ("mqtt_password_file_content", "expected_password"),
     [
         ("secret", "secret"),
         ("secret space", "secret space"),
@@ -115,10 +117,12 @@ def test__main(
         ("你好\n", "你好"),
     ],
 )
-def test__main_password_file(tmpdir, password_file_content, expected_password):
+def test__main_mqtt_password_file(
+    tmpdir, mqtt_password_file_content, expected_password
+):
     mqtt_password_path = tmpdir.join("mqtt-password")
     with mqtt_password_path.open("w") as mqtt_password_file:
-        mqtt_password_file.write(password_file_content)
+        mqtt_password_file.write(mqtt_password_file_content)
     with unittest.mock.patch("switchbot_mqtt._run") as run_mock, unittest.mock.patch(
         "sys.argv",
         [
@@ -139,10 +143,11 @@ def test__main_password_file(tmpdir, password_file_content, expected_password):
         mqtt_username="me",
         mqtt_password=expected_password,
         retry_count=3,
+        device_passwords={},
     )
 
 
-def test__main_password_file_collision(capsys):
+def test__main_mqtt_password_file_collision(capsys):
     with unittest.mock.patch(
         "sys.argv",
         [
@@ -166,3 +171,35 @@ def test__main_password_file_collision(capsys):
         "argument --mqtt-password-file: not allowed with argument --mqtt-password\n"
         in err
     )
+
+
+@pytest.mark.parametrize(
+    "device_passwords",
+    [
+        {},
+        {"11:22:33:44:55:66": "password", "aa:bb:cc:dd:ee:ff": "secret"},
+    ],
+)
+def test__main_device_password_file(tmpdir, device_passwords):
+    device_passwords_path = tmpdir.join("passwords.json")
+    device_passwords_path.write_text(json.dumps(device_passwords), encoding="utf8")
+    with unittest.mock.patch("switchbot_mqtt._run") as run_mock, unittest.mock.patch(
+        "sys.argv",
+        [
+            "",
+            "--mqtt-host",
+            "localhost",
+            "--device-password-file",
+            str(device_passwords_path),
+        ],
+    ):
+        # pylint: disable=protected-access
+        switchbot_mqtt._main()
+    run_mock.assert_called_once_with(
+        mqtt_host="localhost",
+        mqtt_port=1883,
+        mqtt_username=None,
+        mqtt_password=None,
+        retry_count=3,
+        device_passwords=device_passwords,
+    )

+ 10 - 2
tests/test_mqtt.py

@@ -32,7 +32,11 @@ import switchbot_mqtt
 @pytest.mark.parametrize("mqtt_host", ["mqtt-broker.local"])
 @pytest.mark.parametrize("mqtt_port", [1833])
 @pytest.mark.parametrize("retry_count", [3, 21])
-def test__run(caplog, mqtt_host, mqtt_port, retry_count):
+@pytest.mark.parametrize(
+    "device_passwords",
+    [{}, {"11:22:33:44:55:66": "password", "aa:bb:cc:dd:ee:ff": "secret"}],
+)
+def test__run(caplog, mqtt_host, mqtt_port, retry_count, device_passwords):
     with unittest.mock.patch(
         "paho.mqtt.client.Client"
     ) as mqtt_client_mock, caplog.at_level(logging.DEBUG):
@@ -42,10 +46,12 @@ def test__run(caplog, mqtt_host, mqtt_port, retry_count):
             mqtt_username=None,
             mqtt_password=None,
             retry_count=retry_count,
+            device_passwords=device_passwords,
         )
     mqtt_client_mock.assert_called_once_with(
         userdata=switchbot_mqtt._MQTTCallbackUserdata(
-            retry_count=retry_count, device_passwords={}
+            retry_count=retry_count,
+            device_passwords=device_passwords,
         )
     )
     assert not mqtt_client_mock().username_pw_set.called
@@ -104,6 +110,7 @@ def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password)
             mqtt_username=mqtt_username,
             mqtt_password=mqtt_password,
             retry_count=7,
+            device_passwords={},
         )
     mqtt_client_mock.assert_called_once_with(
         userdata=switchbot_mqtt._MQTTCallbackUserdata(
@@ -127,6 +134,7 @@ def test__run_authentication_missing_username(mqtt_host, mqtt_port, mqtt_passwor
                 mqtt_username=None,
                 mqtt_password=mqtt_password,
                 retry_count=3,
+                device_passwords={},
             )