Beispiel #1
0
def test__mqtt_command_callback_password(
        mac_address: str, expected_password: typing.Optional[str]) -> None:
    ActorMock = _mock_actor_class(
        command_topic_levels=("switchbot", _MQTTTopicPlaceholder.MAC_ADDRESS))
    message = MQTTMessage(topic=b"prefix-switchbot/" + mac_address.encode())
    message.payload = b"whatever"
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=3,
        device_passwords={
            "11:22:33:44:55:77": "test",
            "aa:bb:cc:dd:ee:ff": "secret",
            "11:22:33:dd:ee:ff": "äöü",
        },
        fetch_device_info=True,
        mqtt_topic_prefix="prefix-",
    )
    with unittest.mock.patch.object(
            ActorMock, "__init__",
            return_value=None) as init_mock, unittest.mock.patch.object(
                ActorMock, "execute_command") as execute_command_mock:
        ActorMock._mqtt_command_callback("client_dummy", callback_userdata,
                                         message)
    init_mock.assert_called_once_with(mac_address=mac_address,
                                      retry_count=3,
                                      password=expected_password)
    execute_command_mock.assert_called_once_with(
        mqtt_client="client_dummy",
        mqtt_message_payload=b"whatever",
        update_device_info=True,
        mqtt_topic_prefix="prefix-",
    )
Beispiel #2
0
def test__mqtt_command_callback_ignore_retained(
        caplog: _pytest.logging.LogCaptureFixture, topic: bytes,
        payload: bytes) -> None:
    ActorMock = _mock_actor_class(
        command_topic_levels=_ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS)
    message = MQTTMessage(topic=topic)
    message.payload = payload
    message.retain = True
    with unittest.mock.patch.object(
            ActorMock, "__init__",
            return_value=None) as init_mock, unittest.mock.patch.object(
                ActorMock,
                "execute_command") as execute_command_mock, caplog.at_level(
                    logging.DEBUG):
        ActorMock._mqtt_command_callback(
            "client_dummy",
            _MQTTCallbackUserdata(
                retry_count=4,
                device_passwords={},
                fetch_device_info=True,
                mqtt_topic_prefix="homeassistant/",
            ),
            message,
        )
    init_mock.assert_not_called()
    execute_command_mock.assert_not_called()
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors.base",
            logging.DEBUG,
            f"received topic={topic.decode()} payload={payload!r}",
        ),
        ("switchbot_mqtt._actors.base", logging.INFO,
         "ignoring retained message"),
    ]
Beispiel #3
0
def test__mqtt_update_device_info_callback(
    caplog: _pytest.logging.LogCaptureFixture,
    topic_levels: typing.Tuple[_MQTTTopicLevel, ...],
    topic: bytes,
    expected_mac_address: str,
    payload: bytes,
) -> None:
    ActorMock = _mock_actor_class(request_info_levels=topic_levels)
    message = MQTTMessage(topic=topic)
    message.payload = payload
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=21,  # tested in test__mqtt_command_callback
        device_passwords={},
        fetch_device_info=True,
        mqtt_topic_prefix="prfx/",
    )
    with unittest.mock.patch.object(
            ActorMock, "__init__",
            return_value=None) as init_mock, unittest.mock.patch.object(
                ActorMock, "_update_and_report_device_info"
            ) as update_mock, caplog.at_level(logging.DEBUG):
        ActorMock._mqtt_update_device_info_callback("client_dummy",
                                                    callback_userdata, message)
    init_mock.assert_called_once_with(mac_address=expected_mac_address,
                                      retry_count=21,
                                      password=None)
    update_mock.assert_called_once_with(mqtt_client="client_dummy",
                                        mqtt_topic_prefix="prfx/")
    assert caplog.record_tuples == [(
        "switchbot_mqtt._actors.base",
        logging.DEBUG,
        f"received topic={topic.decode()} payload={payload!r}",
    )]
Beispiel #4
0
def test__mqtt_update_device_info_callback_ignore_retained(
    caplog: _pytest.logging.LogCaptureFixture, ) -> None:
    ActorMock = _mock_actor_class(
        request_info_levels=(_MQTTTopicPlaceholder.MAC_ADDRESS, "request"))
    message = MQTTMessage(topic=b"aa:bb:cc:dd:ee:ff/request")
    message.payload = b""
    message.retain = True
    with unittest.mock.patch.object(
            ActorMock, "__init__",
            return_value=None) as init_mock, unittest.mock.patch.object(
                ActorMock,
                "execute_command") as execute_command_mock, caplog.at_level(
                    logging.DEBUG):
        ActorMock._mqtt_update_device_info_callback(
            "client_dummy",
            _MQTTCallbackUserdata(
                retry_count=21,
                device_passwords={},
                fetch_device_info=True,
                mqtt_topic_prefix="ignored",
            ),
            message,
        )
    init_mock.assert_not_called()
    execute_command_mock.assert_not_called()
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors.base",
            logging.DEBUG,
            "received topic=aa:bb:cc:dd:ee:ff/request payload=b''",
        ),
        ("switchbot_mqtt._actors.base", logging.INFO,
         "ignoring retained message"),
    ]
Beispiel #5
0
def test__mqtt_set_position_callback_command_failed(
    caplog: _pytest.logging.LogCaptureFixture, ) -> None:
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=3,
        device_passwords={},
        fetch_device_info=False,
        mqtt_topic_prefix="",
    )
    message = MQTTMessage(
        topic=b"cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent"
    )
    message.payload = b"21"
    with unittest.mock.patch(
            "switchbot.SwitchbotCurtain") as device_init_mock, caplog.at_level(
                logging.INFO):
        device_init_mock().set_position.return_value = False
        device_init_mock.reset_mock()
        _CurtainMotor._mqtt_set_position_callback(mqtt_client="client dummy",
                                                  userdata=callback_userdata,
                                                  message=message)
    device_init_mock.assert_called_once()
    device_init_mock().set_position.assert_called_with(21)
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors",
            logging.ERROR,
            "failed to set position of switchbot curtain aa:bb:cc:dd:ee:ff",
        ),
    ]
Beispiel #6
0
def test__run_authentication(
    mqtt_host: str,
    mqtt_port: int,
    mqtt_username: str,
    mqtt_password: typing.Optional[str],
) -> None:
    with unittest.mock.patch("paho.mqtt.client.Client") as mqtt_client_mock:
        switchbot_mqtt._run(
            mqtt_host=mqtt_host,
            mqtt_port=mqtt_port,
            mqtt_disable_tls=True,
            mqtt_username=mqtt_username,
            mqtt_password=mqtt_password,
            mqtt_topic_prefix="prfx",
            retry_count=7,
            device_passwords={},
            fetch_device_info=True,
        )
    mqtt_client_mock.assert_called_once_with(userdata=_MQTTCallbackUserdata(
        retry_count=7,
        device_passwords={},
        fetch_device_info=True,
        mqtt_topic_prefix="prfx",
    ))
    mqtt_client_mock().username_pw_set.assert_called_once_with(
        username=mqtt_username, password=mqtt_password)
Beispiel #7
0
def test__mqtt_set_position_callback_invalid_position(
    caplog: _pytest.logging.LogCaptureFixture,
    payload: bytes,
) -> None:
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=3,
        device_passwords={},
        fetch_device_info=False,
        mqtt_topic_prefix="homeassistant/",
    )
    message = MQTTMessage(
        topic=
        b"homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent"
    )
    message.payload = payload
    with unittest.mock.patch(
            "switchbot.SwitchbotCurtain") as device_init_mock, caplog.at_level(
                logging.INFO):
        _CurtainMotor._mqtt_set_position_callback(mqtt_client="client dummy",
                                                  userdata=callback_userdata,
                                                  message=message)
    device_init_mock.assert_called_once()
    device_init_mock().set_position.assert_not_called()
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors",
            logging.WARN,
            f"invalid position {payload.decode()}%, ignoring message",
        ),
    ]
Beispiel #8
0
def test__mqtt_set_position_callback_invalid_mac_address(
    caplog: _pytest.logging.LogCaptureFixture, ) -> None:
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=3,
        device_passwords={},
        fetch_device_info=False,
        mqtt_topic_prefix="tnatsissaemoh/",
    )
    message = MQTTMessage(
        topic=
        b"tnatsissaemoh/cover/switchbot-curtain/aa:bb:cc:dd:ee/position/set-percent"
    )
    message.payload = b"42"
    with unittest.mock.patch(
            "switchbot.SwitchbotCurtain") as device_init_mock, caplog.at_level(
                logging.INFO):
        _CurtainMotor._mqtt_set_position_callback(mqtt_client="client dummy",
                                                  userdata=callback_userdata,
                                                  message=message)
    device_init_mock.assert_not_called()
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors.base",
            logging.WARN,
            "invalid mac address aa:bb:cc:dd:ee",
        ),
    ]
Beispiel #9
0
def test__mqtt_set_position_callback_ignore_retained(
    caplog: _pytest.logging.LogCaptureFixture, ) -> None:
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=3,
        device_passwords={},
        fetch_device_info=False,
        mqtt_topic_prefix="whatever",
    )
    message = MQTTMessage(
        topic=
        b"homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent"
    )
    message.payload = b"42"
    message.retain = True
    with unittest.mock.patch(
            "switchbot.SwitchbotCurtain") as device_init_mock, caplog.at_level(
                logging.INFO):
        _CurtainMotor._mqtt_set_position_callback(mqtt_client="client dummy",
                                                  userdata=callback_userdata,
                                                  message=message)
    device_init_mock.assert_not_called()
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors",
            logging.INFO,
            "ignoring retained message on topic"
            " homeassistant/cover/switchbot-curtain/aa:bb:cc:dd:ee:ff/position/set-percent",
        ),
    ]
Beispiel #10
0
def test__mqtt_set_position_callback(
    caplog: _pytest.logging.LogCaptureFixture,
    topic: bytes,
    payload: bytes,
    expected_mac_address: str,
    retry_count: int,
    expected_position_percent: int,
) -> None:
    callback_userdata = _MQTTCallbackUserdata(
        retry_count=retry_count,
        device_passwords={},
        fetch_device_info=False,
        mqtt_topic_prefix="home/",
    )
    message = MQTTMessage(topic=topic)
    message.payload = payload
    with unittest.mock.patch(
            "switchbot.SwitchbotCurtain") as device_init_mock, caplog.at_level(
                logging.DEBUG):
        _CurtainMotor._mqtt_set_position_callback(mqtt_client="client dummy",
                                                  userdata=callback_userdata,
                                                  message=message)
    device_init_mock.assert_called_once_with(
        mac=expected_mac_address,
        password=None,
        retry_count=retry_count,
        reverse_mode=True,
    )
    device_init_mock().set_position.assert_called_once_with(
        expected_position_percent)
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors",
            logging.DEBUG,
            f"received topic=home/cover/switchbot-curtain/{expected_mac_address}"
            f"/position/set-percent payload=b'{expected_position_percent}'",
        ),
        (
            "switchbot_mqtt._actors",
            logging.INFO,
            f"set position of switchbot curtain {expected_mac_address}"
            f" to {expected_position_percent}%",
        ),
    ]
Beispiel #11
0
def test__mqtt_command_callback_invalid_mac_address(
        caplog: _pytest.logging.LogCaptureFixture, mac_address: str,
        payload: bytes) -> None:
    ActorMock = _mock_actor_class(
        command_topic_levels=_ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS)
    topic = f"mqttprefix-switch/switchbot/{mac_address}/set".encode()
    message = MQTTMessage(topic=topic)
    message.payload = payload
    with unittest.mock.patch.object(
            ActorMock, "__init__",
            return_value=None) as init_mock, unittest.mock.patch.object(
                ActorMock,
                "execute_command") as execute_command_mock, caplog.at_level(
                    logging.DEBUG):
        ActorMock._mqtt_command_callback(
            "client_dummy",
            _MQTTCallbackUserdata(
                retry_count=3,
                device_passwords={},
                fetch_device_info=True,
                mqtt_topic_prefix="mqttprefix-",
            ),
            message,
        )
    init_mock.assert_not_called()
    execute_command_mock.assert_not_called()
    assert caplog.record_tuples == [
        (
            "switchbot_mqtt._actors.base",
            logging.DEBUG,
            f"received topic={topic.decode()} payload={payload!r}",
        ),
        (
            "switchbot_mqtt._actors.base",
            logging.WARNING,
            f"invalid mac address {mac_address}",
        ),
    ]
Beispiel #12
0
def _run(
    *,
    mqtt_host: str,
    mqtt_port: int,
    mqtt_disable_tls: bool,
    mqtt_username: typing.Optional[str],
    mqtt_password: typing.Optional[str],
    mqtt_topic_prefix: str,
    retry_count: int,
    device_passwords: typing.Dict[str, str],
    fetch_device_info: bool,
) -> None:
    # https://pypi.org/project/paho-mqtt/
    mqtt_client = paho.mqtt.client.Client(
        userdata=_MQTTCallbackUserdata(
            retry_count=retry_count,
            device_passwords=device_passwords,
            fetch_device_info=fetch_device_info,
            mqtt_topic_prefix=mqtt_topic_prefix,
        )
    )
    mqtt_client.on_connect = _mqtt_on_connect
    _LOGGER.info(
        "connecting to MQTT broker %s:%d (TLS %s)",
        mqtt_host,
        mqtt_port,
        "disabled" if mqtt_disable_tls else "enabled",
    )
    if not mqtt_disable_tls:
        mqtt_client.tls_set(ca_certs=None)  # enable tls trusting default system certs
    if mqtt_username:
        mqtt_client.username_pw_set(username=mqtt_username, password=mqtt_password)
    elif mqtt_password:
        raise ValueError("Missing MQTT username")
    mqtt_client.connect(host=mqtt_host, port=mqtt_port)
    # https://github.com/eclipse/paho.mqtt.python/blob/master/src/paho/mqtt/client.py#L1740
    mqtt_client.loop_forever()
Beispiel #13
0
def test__run(
    caplog: _pytest.logging.LogCaptureFixture,
    mqtt_host: str,
    mqtt_port: int,
    retry_count: int,
    device_passwords: typing.Dict[str, str],
    fetch_device_info: bool,
) -> None:
    with unittest.mock.patch(
            "paho.mqtt.client.Client") as mqtt_client_mock, caplog.at_level(
                logging.DEBUG):
        switchbot_mqtt._run(
            mqtt_host=mqtt_host,
            mqtt_port=mqtt_port,
            mqtt_disable_tls=False,
            mqtt_username=None,
            mqtt_password=None,
            mqtt_topic_prefix="homeassistant/",
            retry_count=retry_count,
            device_passwords=device_passwords,
            fetch_device_info=fetch_device_info,
        )
    mqtt_client_mock.assert_called_once()
    assert not mqtt_client_mock.call_args[0]
    assert set(mqtt_client_mock.call_args[1].keys()) == {"userdata"}
    userdata = mqtt_client_mock.call_args[1]["userdata"]
    assert userdata == _MQTTCallbackUserdata(
        retry_count=retry_count,
        device_passwords=device_passwords,
        fetch_device_info=fetch_device_info,
        mqtt_topic_prefix="homeassistant/",
    )
    assert not mqtt_client_mock().username_pw_set.called
    mqtt_client_mock().tls_set.assert_called_once_with(ca_certs=None)
    mqtt_client_mock().connect.assert_called_once_with(host=mqtt_host,
                                                       port=mqtt_port)
    mqtt_client_mock().socket().getpeername.return_value = (mqtt_host,
                                                            mqtt_port)
    with caplog.at_level(logging.DEBUG):
        mqtt_client_mock().on_connect(mqtt_client_mock(), userdata, {}, 0)
    subscribe_mock = mqtt_client_mock().subscribe
    assert subscribe_mock.call_count == (5 if fetch_device_info else 3)
    for topic in [
            "homeassistant/switch/switchbot/+/set",
            "homeassistant/cover/switchbot-curtain/+/set",
            "homeassistant/cover/switchbot-curtain/+/position/set-percent",
    ]:
        assert unittest.mock.call(topic) in subscribe_mock.call_args_list
    for topic in [
            "homeassistant/switch/switchbot/+/request-device-info",
            "homeassistant/cover/switchbot-curtain/+/request-device-info",
    ]:
        assert (unittest.mock.call(topic)
                in subscribe_mock.call_args_list) == fetch_device_info
    callbacks = {
        c[1]["sub"]: c[1]["callback"]
        for c in mqtt_client_mock().message_callback_add.call_args_list
    }
    assert (  # pylint: disable=comparison-with-callable; intended
        callbacks[
            "homeassistant/cover/switchbot-curtain/+/position/set-percent"] ==
        _CurtainMotor._mqtt_set_position_callback)
    mqtt_client_mock().loop_forever.assert_called_once_with()
    assert caplog.record_tuples[:2] == [
        (
            "switchbot_mqtt",
            logging.INFO,
            f"connecting to MQTT broker {mqtt_host}:{mqtt_port} (TLS enabled)",
        ),
        (
            "switchbot_mqtt",
            logging.DEBUG,
            f"connected to MQTT broker {mqtt_host}:{mqtt_port}",
        ),
    ]
    assert len(caplog.record_tuples) == (7 if fetch_device_info else 5)
    assert (
        "switchbot_mqtt._actors.base",
        logging.INFO,
        "subscribing to MQTT topic 'homeassistant/switch/switchbot/+/set'",
    ) in caplog.record_tuples
    assert (
        "switchbot_mqtt._actors.base",
        logging.INFO,
        "subscribing to MQTT topic 'homeassistant/cover/switchbot-curtain/+/set'",
    ) in caplog.record_tuples