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__run_authentication_missing_username(mqtt_host, mqtt_port, mqtt_password): with unittest.mock.patch("paho.mqtt.client.Client"): with pytest.raises(ValueError): switchbot_mqtt._run( mqtt_host=mqtt_host, mqtt_port=mqtt_port, mqtt_username=None, mqtt_password=mqtt_password, )
def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password): with unittest.mock.patch("paho.mqtt.client.Client") as mqtt_client_mock: switchbot_mqtt._run( mqtt_host=mqtt_host, mqtt_port=mqtt_port, mqtt_username=mqtt_username, mqtt_password=mqtt_password, ) mqtt_client_mock.assert_called_once_with() mqtt_client_mock().username_pw_set.assert_called_once_with( username=mqtt_username, password=mqtt_password )
def test__run_authentication_missing_username(mqtt_host: str, mqtt_port: int, mqtt_password: str) -> None: with unittest.mock.patch("paho.mqtt.client.Client"): with pytest.raises(ValueError): switchbot_mqtt._run( mqtt_host=mqtt_host, mqtt_port=mqtt_port, mqtt_disable_tls=True, mqtt_username=None, mqtt_password=mqtt_password, mqtt_topic_prefix="whatever", retry_count=3, device_passwords={}, fetch_device_info=True, )
def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password): with unittest.mock.patch("paho.mqtt.client.Client") as mqtt_client_mock: switchbot_mqtt._run( mqtt_host=mqtt_host, mqtt_port=mqtt_port, mqtt_username=mqtt_username, mqtt_password=mqtt_password, retry_count=7, device_passwords={}, ) mqtt_client_mock.assert_called_once_with( userdata=switchbot_mqtt._MQTTCallbackUserdata(retry_count=7, device_passwords={})) mqtt_client_mock().username_pw_set.assert_called_once_with( username=mqtt_username, password=mqtt_password)
def test__run_tls(caplog: _pytest.logging.LogCaptureFixture, mqtt_disable_tls: bool) -> None: with unittest.mock.patch( "paho.mqtt.client.Client") as mqtt_client_mock, caplog.at_level( logging.INFO): switchbot_mqtt._run( mqtt_host="mqtt.local", mqtt_port=1234, mqtt_disable_tls=mqtt_disable_tls, mqtt_username=None, mqtt_password=None, mqtt_topic_prefix="prfx", retry_count=21, device_passwords={}, fetch_device_info=True, ) if mqtt_disable_tls: mqtt_client_mock().tls_set.assert_not_called() else: mqtt_client_mock().tls_set.assert_called_once_with(ca_certs=None) if mqtt_disable_tls: assert caplog.record_tuples[0][2].endswith(" (TLS disabled)") else: assert caplog.record_tuples[0][2].endswith(" (TLS enabled)")
def test__run(mqtt_host, mqtt_port): with unittest.mock.patch( "paho.mqtt.client.Client" ) as mqtt_client_mock, unittest.mock.patch( "switchbot_mqtt._mqtt_on_message" ) as message_handler_mock: switchbot_mqtt._run( mqtt_host=mqtt_host, mqtt_port=mqtt_port, mqtt_username=None, mqtt_password=None, ) mqtt_client_mock.assert_called_once_with() assert not mqtt_client_mock().username_pw_set.called 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) mqtt_client_mock().on_connect(mqtt_client_mock(), None, {}, 0) mqtt_client_mock().subscribe.assert_called_once_with( "homeassistant/switch/switchbot/+/set" ) mqtt_client_mock().on_message(mqtt_client_mock(), None, "message") # assert_called_once new in python3.6 assert message_handler_mock.call_count == 1 mqtt_client_mock().loop_forever.assert_called_once_with()
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
def _main() -> None: argparser = argparse.ArgumentParser( description="MQTT client controlling SwitchBot button automators, " "compatible with home-assistant.io's MQTT Switch platform" ) argparser.add_argument("--mqtt-host", type=str, required=True) argparser.add_argument( "--mqtt-port", type=int, help=f"default {_MQTT_DEFAULT_PORT} ({_MQTT_DEFAULT_TLS_PORT} with --mqtt-enable-tls)", ) mqtt_tls_argument_group = argparser.add_mutually_exclusive_group() mqtt_tls_argument_group.add_argument( "--mqtt-enable-tls", action="store_true", help="TLS will be enabled by default in the next major release", ) mqtt_tls_argument_group.add_argument( # for upward compatibility "--mqtt-disable-tls", action="store_true", help="Currently enabled by default" ) argparser.add_argument("--mqtt-username", type=str) password_argument_group = argparser.add_mutually_exclusive_group() password_argument_group.add_argument("--mqtt-password", type=str) password_argument_group.add_argument( "--mqtt-password-file", type=pathlib.Path, metavar="PATH", dest="mqtt_password_path", help="Stripping trailing newline", ) argparser.add_argument( "--mqtt-topic-prefix", metavar="PREFIX", default="homeassistant/", # for historic reasons (change to empty string?) help="Default: %(default)s", ) 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", type=int, default=switchbot.DEFAULT_RETRY_COUNT, help="Maximum number of attempts to send a command to a SwitchBot device" " (default: %(default)d)", ) argparser.add_argument( "--fetch-device-info", action="store_true", help="Report devices' battery level on topic " + _ButtonAutomator.get_mqtt_battery_percentage_topic( prefix="[PREFIX]", mac_address="MAC_ADDRESS" ) + " or, respectively, " + _CurtainMotor.get_mqtt_battery_percentage_topic( prefix="[PREFIX]", mac_address="MAC_ADDRESS" ) + " after every command. Additionally report curtain motors' position on topic " + _CurtainMotor.get_mqtt_position_topic( prefix="[PREFIX]", mac_address="MAC_ADDRESS" ) + " after executing stop commands." " When this option is enabled, the mentioned reports may also be requested" " by sending a MQTT message to the topic " + _ButtonAutomator.get_mqtt_update_device_info_topic( prefix="[PREFIX]", mac_address="MAC_ADDRESS" ) + " or " + _CurtainMotor.get_mqtt_update_device_info_topic( prefix="[PREFIX]", mac_address="MAC_ADDRESS" ) + ". This option can also be enabled by assigning a non-empty value to the" " environment variable FETCH_DEVICE_INFO." " [PREFIX] can be set via --mqtt-topic-prefix.", ) argparser.add_argument("--debug", action="store_true") args = argparser.parse_args() # https://github.com/fphammerle/python-cc1101/blob/26d8122661fc4587ecc7c73df55b92d05cf98fe8/cc1101/_cli.py#L51 logging.basicConfig( level=logging.DEBUG if args.debug else logging.INFO, format="%(asctime)s:%(levelname)s:%(name)s:%(funcName)s:%(message)s" if args.debug else "%(message)s", datefmt="%Y-%m-%dT%H:%M:%S%z", ) _LOGGER.debug("args=%r", args) if args.mqtt_port: mqtt_port = args.mqtt_port elif args.mqtt_enable_tls: mqtt_port = _MQTT_DEFAULT_TLS_PORT else: mqtt_port = _MQTT_DEFAULT_PORT if not args.mqtt_enable_tls and not args.mqtt_disable_tls: warnings.warn( "In switchbot-mqtt's next major release, TLS will be enabled by default" " (--mqtt-enable-tls)." " Please add --mqtt-disable-tls to your command for upward compatibility.", UserWarning, # DeprecationWarning ignored by default ) if args.mqtt_password_path: # .read_text() replaces \r\n with \n mqtt_password = args.mqtt_password_path.read_bytes().decode() if mqtt_password.endswith("\r\n"): mqtt_password = mqtt_password[:-2] elif mqtt_password.endswith("\n"): 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 = {} switchbot_mqtt._run( # pylint: disable=protected-access; internal mqtt_host=args.mqtt_host, mqtt_port=mqtt_port, mqtt_disable_tls=not args.mqtt_enable_tls, mqtt_username=args.mqtt_username, mqtt_password=mqtt_password, mqtt_topic_prefix=args.mqtt_topic_prefix, retry_count=args.retry_count, device_passwords=device_passwords, fetch_device_info=args.fetch_device_info # > In formal language theory, the empty string, [...], is the unique string of length zero. # https://en.wikipedia.org/wiki/Empty_string or bool(os.environ.get("FETCH_DEVICE_INFO")), )
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): switchbot_mqtt._run( mqtt_host=mqtt_host, mqtt_port=mqtt_port, 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=device_passwords, )) assert not mqtt_client_mock().username_pw_set.called 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(), None, {}, 0) assert mqtt_client_mock().subscribe.call_args_list == [ unittest.mock.call("homeassistant/switch/switchbot/+/set"), unittest.mock.call("homeassistant/cover/switchbot-curtain/+/set"), ] assert mqtt_client_mock().message_callback_add.call_args_list == [ unittest.mock.call( sub="homeassistant/switch/switchbot/+/set", callback=switchbot_mqtt._ButtonAutomator._mqtt_command_callback, ), unittest.mock.call( sub="homeassistant/cover/switchbot-curtain/+/set", callback=switchbot_mqtt._CurtainMotor._mqtt_command_callback, ), ] mqtt_client_mock().loop_forever.assert_called_once_with() assert caplog.record_tuples == [ ( "switchbot_mqtt", logging.INFO, "connecting to MQTT broker {}:{}".format(mqtt_host, mqtt_port), ), ( "switchbot_mqtt", logging.DEBUG, "connected to MQTT broker {}:{}".format(mqtt_host, mqtt_port), ), ( "switchbot_mqtt", logging.INFO, "subscribing to MQTT topic 'homeassistant/switch/switchbot/+/set'", ), ( "switchbot_mqtt", logging.INFO, "subscribing to MQTT topic 'homeassistant/cover/switchbot-curtain/+/set'", ), ]