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-", )
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"), ]
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}", )]
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"), ]
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", ), ]
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)
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", ), ]
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", ), ]
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", ), ]
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}%", ), ]
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}", ), ]
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()
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