diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index cc2c74c359..1289923667 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -229,19 +229,19 @@ { "alias": "value_card", "name": "Value card", - "image": null, + "image": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyNyIgZmlsbD0ibm9uZSIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTI4IDEyNyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfMTE0Ml8yMDM5NTQpIj4KICA8cmVjdCB4PSI1LjUiIHk9IjIuNSIgd2lkdGg9IjExNyIgaGVpZ2h0PSIxMTciIHJ4PSIyLjI5NDEiIGZpbGw9IiNmZmYiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPgogIDxwYXRoIGQ9Im0zMy42MDMgMjkuMjIxdi03LjY0NzFjMC0xLjU4NjgtMS4yODA4LTIuODY3Ni0yLjg2NzYtMi44Njc2cy0yLjg2NzcgMS4yODA4LTIuODY3NyAyLjg2NzZ2Ny42NDcxYy0xLjE1NjYgMC44Njk4LTEuOTExNyAyLjI2NTQtMS45MTE3IDMuODIzNSAwIDIuNjM4MiAyLjE0MTIgNC43Nzk0IDQuNzc5NCA0Ljc3OTRzNC43Nzk0LTIuMTQxMiA0Ljc3OTQtNC43Nzk0YzAtMS41NTgxLTAuNzU1MS0yLjk1MzctMS45MTE4LTMuODIzNXptLTMuODIzNS03LjY0NzFjMC0wLjUyNTcgMC40MzAyLTAuOTU1OSAwLjk1NTktMC45NTU5czAuOTU1OSAwLjQzMDIgMC45NTU5IDAuOTU1OWgtMC45NTU5djAuOTU1OWgwLjk1NTl2MS45MTE3aC0wLjk1NTl2MC45NTU5aDAuOTU1OXYxLjkxMThoLTEuOTExOHYtNS43MzUzeiIgZmlsbD0iIzU0NjlGRiIvPgogIDxnIGZpbGw9IiMwMDAiPgogICA8cGF0aCBkPSJtNTAuMTQxIDE5Ljc0MXY2LjUyMzhoLTEuMTE1N3YtNi41MjM4aDEuMTE1N3ptMi4wNDc3IDB2MC44OTYxaC01LjE5MzJ2LTAuODk2MWg1LjE5MzJ6bTIuNjAzMyA2LjYxMzVjLTAuMzU4NSAwLTAuNjgyNi0wLjA1ODMtMC45NzIzLTAuMTc0OC0wLjI4NjgtMC4xMTk1LTAuNTMxOC0wLjI4NTMtMC43MzQ5LTAuNDk3My0wLjIwMDEtMC4yMTIxLTAuMzU0LTAuNDYxNi0wLjQ2MTUtMC43NDgzLTAuMTA3NS0wLjI4NjgtMC4xNjEzLTAuNTk2LTAuMTYxMy0wLjkyNzV2LTAuMTc5M2MwLTAuMzc5MyAwLjA1NTMtMC43MjI4IDAuMTY1OC0xLjAzMDVzMC4yNjQzLTAuNTcwNiAwLjQ2MTUtMC43ODg2YzAuMTk3MS0wLjIyMTEgMC40MzAxLTAuMzg5OCAwLjY5OS0wLjUwNjMgMC4yNjg4LTAuMTE2NSAwLjU2MDEtMC4xNzQ4IDAuODczNy0wLjE3NDggMC4zNDY1IDAgMC42NDk3IDAuMDU4MyAwLjkwOTYgMC4xNzQ4czAuNDc1IDAuMjgwOCAwLjY0NTIgMC40OTI4YzAuMTczMyAwLjIwOTEgMC4zMDE3IDAuNDU4NiAwLjM4NTQgMC43NDgzIDAuMDg2NiAwLjI4OTggMC4xMjk5IDAuNjA5NCAwLjEyOTkgMC45NTg5djAuNDYxNWgtMy43NDU5di0wLjc3NTJoMi42Nzk1di0wLjA4NTFjLTZlLTMgLTAuMTk0Mi0wLjA0NDgtMC4zNzY0LTAuMTE2NS0wLjU0NjYtMC4wNjg3LTAuMTcwMy0wLjE3NDctMC4zMDc3LTAuMzE4MS0wLjQxMjMtMC4xNDM0LTAuMTA0NS0wLjMzNDYtMC4xNTY4LTAuNTczNi0wLjE1NjgtMC4xNzkyIDAtMC4zMzkgMC4wMzg4LTAuNDc5NCAwLjExNjUtMC4xMzc0IDAuMDc0Ny0wLjI1MjQgMC4xODM3LTAuMzQ1IDAuMzI3MXMtMC4xNjQzIDAuMzE2Ni0wLjIxNTEgMC41MTk4Yy0wLjA0NzggMC4yMDAxLTAuMDcxNyAwLjQyNTYtMC4wNzE3IDAuNjc2NXYwLjE3OTNjMCAwLjIxMjEgMC4wMjg0IDAuNDA5MiAwLjA4NTIgMC41OTE0IDAuMDU5NyAwLjE3OTMgMC4xNDYzIDAuMzM2MSAwLjI1OTggMC40NzA1IDAuMTEzNiAwLjEzNDQgMC4yNTEgMC4yNDA1IDAuNDEyMyAwLjMxODEgMC4xNjEzIDAuMDc0NyAwLjM0NSAwLjExMiAwLjU1MTEgMC4xMTIgMC4yNTk5IDAgMC40OTE0LTAuMDUyMiAwLjY5NDUtMC4xNTY4IDAuMjAzMS0wLjEwNDUgMC4zNzk0LTAuMjUyNCAwLjUyODctMC40NDM2bDAuNTY5MSAwLjU1MTJjLTAuMTA0NiAwLjE1MjMtMC4yNDA1IDAuMjk4Ny0wLjQwNzggMC40MzkxLTAuMTY3MyAwLjEzNzQtMC4zNzE5IDAuMjQ5NC0wLjYxMzggMC4zMzYtMC4yMzkgMC4wODY2LTAuNTE2OCAwLjEzLTAuODMzNCAwLjEzem00LjAwNTctMy45NTJ2My44NjIzaC0xLjA3OTh2LTQuODQ4MWgxLjAxNzFsMC4wNjI3IDAuOTg1OHptLTAuMTc0NyAxLjI1OTEtMC4zNjc1LTAuMDA0NWMwLTAuMzM0NiAwLjA0MTktMC42NDM3IDAuMTI1NS0wLjkyNzVzMC4yMDYxLTAuNTMwMiAwLjM2NzQtMC43MzkzYzAuMTYxMy0wLjIxMjEgMC4zNjE1LTAuMzc0OSAwLjYwMDQtMC40ODg0IDAuMjQyLTAuMTE2NSAwLjUyMTMtMC4xNzQ4IDAuODM3OS0wLjE3NDggMC4yMjExIDAgMC40MjI3IDAuMDMyOSAwLjYwNDkgMC4wOTg2IDAuMTg1MiAwLjA2MjcgMC4zNDUgMC4xNjI4IDAuNDc5NSAwLjMwMDIgMC4xMzc0IDAuMTM3NCAwLjI0MTkgMC4zMTM2IDAuMzEzNiAwLjUyODcgMC4wNzQ3IDAuMjE1MSAwLjExMiAwLjQ3NSAwLjExMiAwLjc3OTd2My4yMzA1aC0xLjA3OTh2LTMuMTM2NGMwLTAuMjM2LTAuMDM1OS0wLjQyMTItMC4xMDc2LTAuNTU1Ni0wLjA2ODctMC4xMzQ1LTAuMTY4Ny0wLjIzMDEtMC4zMDAyLTAuMjg2OC0wLjEyODQtMC4wNTk4LTAuMjgyMy0wLjA4OTYtMC40NjE1LTAuMDg5Ni0wLjIwMzEgMC0wLjM3NjQgMC4wMzg4LTAuNTE5NyAwLjExNjUtMC4xNDA0IDAuMDc3Ni0wLjI1NTQgMC4xODM3LTAuMzQ1MSAwLjMxODEtMC4wODk2IDAuMTM0NC0wLjE1NTMgMC4yODk4LTAuMTk3MSAwLjQ2NnMtMC4wNjI3IDAuMzY0NC0wLjA2MjcgMC41NjQ2em0zLjAwNjUtMC4yODY4LTAuNTA2MyAwLjExMmMwLTAuMjkyNyAwLjA0MDMtMC41NjkgMC4xMjEtMC44Mjg5IDAuMDgzNi0wLjI2MjkgMC4yMDQ2LTAuNDkyOSAwLjM2MjktMC42OSAwLjE2MTMtMC4yMDAyIDAuMzYtMC4zNTcgMC41OTU5LTAuNDcwNSAwLjIzNi0wLjExMzUgMC41MDY0LTAuMTcwMyAwLjgxMS0wLjE3MDMgMC4yNDggMCAwLjQ2OSAwLjAzNDQgMC42NjMyIDAuMTAzMSAwLjE5NzEgMC4wNjU3IDAuMzY0NCAwLjE3MDIgMC41MDE4IDAuMzEzNnMwLjI0MiAwLjMzMDEgMC4zMTM3IDAuNTYwMWMwLjA3MTcgMC4yMjcgMC4xMDc1IDAuNTAxOCAwLjEwNzUgMC44MjQ1djMuMTM2NGgtMS4wODQzdi0zLjE0MDljMC0wLjI0NS0wLjAzNTktMC40MzQ2LTAuMTA3Ni0wLjU2OTEtMC4wNjg3LTAuMTM0NC0wLjE2NzItMC4yMjctMC4yOTU3LTAuMjc3OC0wLjEyODQtMC4wNTM3LTAuMjgyMy0wLjA4MDYtMC40NjE1LTAuMDgwNi0wLjE2NzMgMC0wLjMxNTEgMC4wMzEzLTAuNDQzNiAwLjA5NDEtMC4xMjU0IDAuMDU5Ny0wLjIzMTUgMC4xNDQ4LTAuMzE4MSAwLjI1NTQtMC4wODY2IDAuMTA3NS0wLjE1MjQgMC4yMzE1LTAuMTk3MiAwLjM3MTktMC4wNDE4IDAuMTQwNC0wLjA2MjcgMC4yOTI3LTAuMDYyNyAwLjQ1N3ptNS4zMDk2LTEuMDI2MXY1Ljc4MDFoLTEuMDc5OHYtNi43MTIxaDAuOTk0N2wwLjA4NTEgMC45MzJ6bTMuMTU4OSAxLjQ0NzN2MC4wOTQxYzAgMC4zNTI1LTAuMDQxOCAwLjY3OTYtMC4xMjU0IDAuOTgxMy0wLjA4MDcgMC4yOTg3LTAuMjAxNyAwLjU2LTAuMzYzIDAuNzg0MS0wLjE1ODMgMC4yMjEtMC4zNTM5IDAuMzkyOC0wLjU4NjkgMC41MTUzLTAuMjMzIDAuMTIyNC0wLjUwMTkgMC4xODM3LTAuODA2NiAwLjE4MzctMC4zMDE3IDAtMC41NjYtMC4wNTUzLTAuNzkzLTAuMTY1OC0wLjIyNDEtMC4xMTM1LTAuNDEzOC0wLjI3MzMtMC41NjkxLTAuNDc5NS0wLjE1NTMtMC4yMDYxLTAuMjgwOC0wLjQ0OC0wLjM3NjQtMC43MjU4LTAuMDkyNi0wLjI4MDgtMC4xNTgzLTAuNTg4NS0wLjE5NzEtMC45MjMxdi0wLjM2MjljMC4wMzg4LTAuMzU1NSAwLjEwNDUtMC42NzgxIDAuMTk3MS0wLjk2NzggMC4wOTU2LTAuMjg5OCAwLjIyMTEtMC41MzkyIDAuMzc2NC0wLjc0ODNzMC4zNDUtMC4zNzA0IDAuNTY5MS0wLjQ4MzljMC4yMjQtMC4xMTM1IDAuNDg1NC0wLjE3MDMgMC43ODQxLTAuMTcwMyAwLjMwNDcgMCAwLjU3NSAwLjA1OTggMC44MTEgMC4xNzkyIDAuMjM2IDAuMTE2NSAwLjQzNDYgMC4yODM4IDAuNTk1OSAwLjUwMTkgMC4xNjEzIDAuMjE1MSAwLjI4MjMgMC40NzQ5IDAuMzYyOSAwLjc3OTYgMC4wODA3IDAuMzAxNyAwLjEyMSAwLjYzNzggMC4xMjEgMS4wMDgyem0tMS4wNzk4IDAuMDk0MXYtMC4wOTQxYzAtMC4yMjQxLTAuMDIwOS0wLjQzMTctMC4wNjI3LTAuNjIyOC0wLjA0MTktMC4xOTQyLTAuMTA3Ni0wLjM2NDUtMC4xOTcyLTAuNTEwOC0wLjA4OTYtMC4xNDY0LTAuMjA0Ni0wLjI1OTktMC4zNDUtMC4zNDA2LTAuMTM3NC0wLjA4MzYtMC4zMDMyLTAuMTI1NC0wLjQ5NzQtMC4xMjU0LTAuMTkxMSAwLTAuMzU1NCAwLjAzMjgtMC40OTI4IDAuMDk4NS0wLjEzNzUgMC4wNjI4LTAuMjUyNSAwLjE1MDktMC4zNDUxIDAuMjY0NHMtMC4xNjQzIDAuMjQ2NC0wLjIxNSAwLjM5ODhjLTAuMDUwOCAwLjE0OTMtMC4wODY3IDAuMzEyMS0wLjEwNzYgMC40ODg0djAuODY5MmMwLjAzNTkgMC4yMTUxIDAuMDk3MSAwLjQxMjMgMC4xODM3IDAuNTkxNSAwLjA4NjcgMC4xNzkyIDAuMjA5MSAwLjMyMjYgMC4zNjc1IDAuNDMwMSAwLjE2MTMgMC4xMDQ2IDAuMzY3NCAwLjE1NjkgMC42MTgzIDAuMTU2OSAwLjE5NDIgMCAwLjM1OTktMC4wNDE5IDAuNDk3My0wLjEyNTUgMC4xMzc1LTAuMDgzNiAwLjI0OTUtMC4xOTg2IDAuMzM2MS0wLjM0NSAwLjA4OTYtMC4xNDk0IDAuMTU1My0wLjMyMTEgMC4xOTcyLTAuNTE1MyAwLjA0MTgtMC4xOTQxIDAuMDYyNy0wLjQwMDMgMC4wNjI3LTAuNjE4M3ptNC4yNzkgMi40NjQ0Yy0wLjM1ODQgMC0wLjY4MjUtMC4wNTgzLTAuOTcyMy0wLjE3NDgtMC4yODY3LTAuMTE5NS0wLjUzMTctMC4yODUzLTAuNzM0OC0wLjQ5NzMtMC4yMDAxLTAuMjEyMS0wLjM1NC0wLjQ2MTYtMC40NjE1LTAuNzQ4My0wLjEwNzUtMC4yODY4LTAuMTYxMy0wLjU5Ni0wLjE2MTMtMC45Mjc1di0wLjE3OTNjMC0wLjM3OTMgMC4wNTUyLTAuNzIyOCAwLjE2NTgtMS4wMzA1IDAuMTEwNS0wLjMwNzcgMC4yNjQzLTAuNTcwNiAwLjQ2MTUtMC43ODg2IDAuMTk3MS0wLjIyMTEgMC40MzAxLTAuMzg5OCAwLjY5OS0wLjUwNjMgMC4yNjg4LTAuMTE2NSAwLjU2MDEtMC4xNzQ4IDAuODczNy0wLjE3NDggMC4zNDY1IDAgMC42NDk3IDAuMDU4MyAwLjkwOTYgMC4xNzQ4czAuNDc0OSAwLjI4MDggMC42NDUyIDAuNDkyOGMwLjE3MzMgMC4yMDkxIDAuMzAxNyAwLjQ1ODYgMC4zODUzIDAuNzQ4MyAwLjA4NjcgMC4yODk4IDAuMTMgMC42MDk0IDAuMTMgMC45NTg5djAuNDYxNWgtMy43NDU5di0wLjc3NTJoMi42Nzk1di0wLjA4NTFjLTZlLTMgLTAuMTk0Mi0wLjA0NDgtMC4zNzY0LTAuMTE2NS0wLjU0NjYtMC4wNjg3LTAuMTcwMy0wLjE3NDgtMC4zMDc3LTAuMzE4MS0wLjQxMjMtMC4xNDM0LTAuMTA0NS0wLjMzNDYtMC4xNTY4LTAuNTczNi0wLjE1NjgtMC4xNzkyIDAtMC4zMzkgMC4wMzg4LTAuNDc5NCAwLjExNjUtMC4xMzc0IDAuMDc0Ny0wLjI1MjQgMC4xODM3LTAuMzQ1IDAuMzI3MXMtMC4xNjQzIDAuMzE2Ni0wLjIxNTEgMC41MTk4Yy0wLjA0NzggMC4yMDAxLTAuMDcxNyAwLjQyNTYtMC4wNzE3IDAuNjc2NXYwLjE3OTNjMCAwLjIxMjEgMC4wMjg0IDAuNDA5MiAwLjA4NTEgMC41OTE0IDAuMDU5OCAwLjE3OTMgMC4xNDY0IDAuMzM2MSAwLjI1OTkgMC40NzA1czAuMjUwOSAwLjI0MDUgMC40MTIzIDAuMzE4MWMwLjE2MTMgMC4wNzQ3IDAuMzQ1IDAuMTEyIDAuNTUxMSAwLjExMiAwLjI1OTkgMCAwLjQ5MTQtMC4wNTIyIDAuNjk0NS0wLjE1NjggMC4yMDMxLTAuMTA0NSAwLjM3OTQtMC4yNTI0IDAuNTI4Ny0wLjQ0MzZsMC41NjkxIDAuNTUxMmMtMC4xMDQ2IDAuMTUyMy0wLjI0MDUgMC4yOTg3LTAuNDA3OCAwLjQzOTEtMC4xNjczIDAuMTM3NC0wLjM3MTkgMC4yNDk0LTAuNjEzOCAwLjMzNi0wLjIzOSAwLjA4NjYtMC41MTY4IDAuMTMtMC44MzM1IDAuMTN6bTQuMDEwMy00LjAxNDd2My45MjVoLTEuMDc5OXYtNC44NDgxaDEuMDMwNmwwLjA0OTMgMC45MjMxem0xLjQ4MzEtMC45NTQ0LTllLTMgMS4wMDM2Yy0wLjA2NTctMC4wMTE5LTAuMTM3NC0wLjAyMDktMC4yMTUxLTAuMDI2OC0wLjA3NDYtNmUtMyAtMC4xNDkzLTllLTMgLTAuMjI0LTllLTMgLTAuMTg1MiAwLTAuMzQ4IDAuMDI2OS0wLjQ4ODQgMC4wODA3LTAuMTQwNCAwLjA1MDctMC4yNTg0IDAuMTI1NC0wLjM1NCAwLjIyNC0wLjA5MjYgMC4wOTU2LTAuMTY0MyAwLjIxMjEtMC4yMTUgMC4zNDk1LTAuMDUwOCAwLjEzNzQtMC4wODA3IDAuMjkxMi0wLjA4OTYgMC40NjE1bC0wLjI0NjUgMC4wMTc5YzAtMC4zMDQ3IDAuMDI5OS0wLjU4NyAwLjA4OTYtMC44NDY4IDAuMDU5OC0wLjI1OTkgMC4xNDk0LTAuNDg4NCAwLjI2ODktMC42ODU2IDAuMTIyNC0wLjE5NzEgMC4yNzQ4LTAuMzUxIDAuNDU3LTAuNDYxNSAwLjE4NTItMC4xMTA1IDAuMzk4OC0wLjE2NTggMC42NDA3LTAuMTY1OCAwLjA2NTggMCAwLjEzNiA2ZS0zIDAuMjEwNiAwLjAxNzkgMC4wNzc3IDAuMDEyIDAuMTM2IDAuMDI1NCAwLjE3NDggMC4wNDA0em0zLjM5MTkgMy45MDcxdi0yLjMxMmMwLTAuMTczMy0wLjAzMTQtMC4zMjI2LTAuMDk0MS0wLjQ0ODEtMC4wNjI4LTAuMTI1NC0wLjE1ODMtMC4yMjI1LTAuMjg2OC0wLjI5MTItMC4xMjU0LTAuMDY4Ny0wLjI4MzgtMC4xMDMxLTAuNDc0OS0wLjEwMzEtMC4xNzYzIDAtMC4zMjg2IDAuMDI5OS0wLjQ1NzEgMC4wODk2LTAuMTI4NCAwLjA1OTgtMC4yMjg1IDAuMTQwNC0wLjMwMDIgMC4yNDJzLTAuMTA3NSAwLjIxNjYtMC4xMDc1IDAuMzQ1aC0xLjA3NTRjMC0wLjE5MTIgMC4wNDYzLTAuMzc2NCAwLjEzODktMC41NTU2czAuMjI3LTAuMzM5IDAuNDAzMy0wLjQ3OTRjMC4xNzYyLTAuMTQwNCAwLjM4NjgtMC4yNTA5IDAuNjMxOC0wLjMzMTYgMC4yNDQ5LTAuMDgwNyAwLjUxOTctMC4xMjEgMC44MjQ0LTAuMTIxIDAuMzY0NCAwIDAuNjg3IDAuMDYxMyAwLjk2NzggMC4xODM3IDAuMjgzOCAwLjEyMjUgMC41MDY0IDAuMzA3NyAwLjY2NzcgMC41NTU2IDAuMTY0MyAwLjI0NSAwLjI0NjQgMC41NTI3IDAuMjQ2NCAwLjkyMzF2Mi4xNTUyYzAgMC4yMjEgMC4wMTQ5IDAuNDE5NyAwLjA0NDggMC41OTU5IDAuMDMyOSAwLjE3MzMgMC4wNzkyIDAuMzI0MSAwLjEzODkgMC40NTI2djAuMDcxNmgtMS4xMDY3Yy0wLjA1MDgtMC4xMTY0LTAuMDkxMS0wLjI2NDMtMC4xMjEtMC40NDM1LTAuMDI2OS0wLjE4MjMtMC4wNDAzLTAuMzU4NS0wLjA0MDMtMC41Mjg4em0wLjE1NjgtMS45NzYgOWUtMyAwLjY2NzdoLTAuNzc1MmMtMC4yMDAxIDAtMC4zNzY0IDAuMDE5NC0wLjUyODcgMC4wNTgyLTAuMTUyNCAwLjAzNTktMC4yNzkzIDAuMDg5Ni0wLjM4MDkgMC4xNjEzLTAuMTAxNSAwLjA3MTctMC4xNzc3IDAuMTU4My0wLjIyODUgMC4yNTk5cy0wLjA3NjIgMC4yMTY2LTAuMDc2MiAwLjM0NWMwIDAuMTI4NSAwLjAyOTkgMC4yNDY1IDAuMDg5NiAwLjM1NCAwLjA1OTggMC4xMDQ1IDAuMTQ2NCAwLjE4NjcgMC4yNTk5IDAuMjQ2NCAwLjExNjUgMC4wNTk4IDAuMjU2OSAwLjA4OTYgMC40MjEyIDAuMDg5NiAwLjIyMTEgMCAwLjQxMzctMC4wNDQ4IDAuNTc4LTAuMTM0NCAwLjE2NzMtMC4wOTI2IDAuMjk4Ny0wLjIwNDYgMC4zOTQzLTAuMzM2IDAuMDk1Ni0wLjEzNDQgMC4xNDY0LTAuMjYxNCAwLjE1MjQtMC4zODA5bDAuMzQ5NSAwLjQ3OTVjLTAuMDM1OSAwLjEyMjQtMC4wOTcxIDAuMjUzOS0wLjE4MzggMC4zOTQzLTAuMDg2NiAwLjE0MDMtMC4yMDAxIDAuMjc0OC0wLjM0MDUgMC40MDMyLTAuMTM3NCAwLjEyNTUtMC4zMDMyIDAuMjI4NS0wLjQ5NzMgMC4zMDkyLTAuMTkxMiAwLjA4MDYtMC40MTIzIDAuMTIxLTAuNjYzMiAwLjEyMS0wLjMxNjYgMC0wLjU5ODktMC4wNjI4LTAuODQ2OC0wLjE4ODItMC4yNDgtMC4xMjg1LTAuNDQyMS0wLjMwMDItMC41ODI1LTAuNTE1My0wLjE0MDQtMC4yMTgxLTAuMjEwNi0wLjQ2NDUtMC4yMTA2LTAuNzM5MyAwLTAuMjU2OSAwLjA0NzgtMC40ODM5IDAuMTQzNC0wLjY4MTEgMC4wOTg1LTAuMjAwMSAwLjI0MTktMC4zNjc0IDAuNDMwMS0wLjUwMTggMC4xOTEyLTAuMTM0NCAwLjQyNDItMC4yMzYgMC42OTktMC4zMDQ3IDAuMjc0OC0wLjA3MTcgMC41ODg1LTAuMTA3NiAwLjk0MDktMC4xMDc2aDAuODQ2OXptNC40MjI0LTEuODk5OHYwLjc4ODZoLTIuNzMzMnYtMC43ODg2aDIuNzMzMnptLTEuOTQ0Ni0xLjE4NzRoMS4wNzk5djQuNjk1OGMwIDAuMTQ5NCAwLjAyMDkgMC4yNjQ0IDAuMDYyNyAwLjM0NSAwLjA0NDggMC4wNzc3IDAuMTA2IDAuMTMgMC4xODM3IDAuMTU2OSAwLjA3NzcgMC4wMjY4IDAuMTY4OCAwLjA0MDMgMC4yNzMzIDAuMDQwMyAwLjA3NDcgMCAwLjE0NjQtMC4wMDQ1IDAuMjE1MS0wLjAxMzUgMC4wNjg3LTAuMDA4OSAwLjEyNC0wLjAxNzkgMC4xNjU4LTAuMDI2OGwwLjAwNDUgMC44MjQ0Yy0wLjA4OTYgMC4wMjY5LTAuMTk0MiAwLjA1MDgtMC4zMTM3IDAuMDcxNy0wLjExNjUgMC4wMjA5LTAuMjUwOSAwLjAzMTQtMC40MDMyIDAuMDMxNC0wLjI0OCAwLTAuNDY3NS0wLjA0MzQtMC42NTg3LTAuMTMtMC4xOTEyLTAuMDg5Ni0wLjM0MDUtMC4yMzQ1LTAuNDQ4MS0wLjQzNDYtMC4xMDc1LTAuMjAwMS0wLjE2MTMtMC40NjYtMC4xNjEzLTAuNzk3NnYtNC43NjN6bTUuODM4NCA0Ljg5M3YtMy43MDU2aDEuMDg0M3Y0Ljg0ODFoLTEuMDIxNmwtMC4wNjI3LTEuMTQyNXptMC4xNTIzLTEuMDA4MiAwLjM2My0wLjAwODljMCAwLjMyNTUtMC4wMzU5IDAuNjI1OC0wLjEwNzYgMC45MDA2LTAuMDcxNyAwLjI3MTgtMC4xODIyIDAuNTA5My0wLjMzMTYgMC43MTI0LTAuMTQ5MyAwLjIwMDEtMC4zNDA1IDAuMzU3LTAuNTczNSAwLjQ3MDUtMC4yMzMgMC4xMTA1LTAuNTEyMyAwLjE2NTgtMC44Mzc5IDAuMTY1OC0wLjIzNiAwLTAuNDUyNS0wLjAzNDQtMC42NDk3LTAuMTAzMS0wLjE5NzEtMC4wNjg3LTAuMzY3NC0wLjE3NDctMC41MTA4LTAuMzE4MS0wLjE0MDQtMC4xNDM0LTAuMjQ5NC0wLjMzMDEtMC4zMjcxLTAuNTYwMS0wLjA3NzYtMC4yMy0wLjExNjUtMC41MDQ4LTAuMTE2NS0wLjgyNDV2LTMuMTMyaDEuMDc5OXYzLjE0MWMwIDAuMTc2MiAwLjAyMDkgMC4zMjQxIDAuMDYyNyAwLjQ0MzYgMC4wNDE4IDAuMTE2NSAwLjA5ODYgMC4yMTA2IDAuMTcwMyAwLjI4MjNzMC4xNTUzIDAuMTIyNCAwLjI1MDkgMC4xNTIzIDAuMTk3MSAwLjA0NDggMC4zMDQ3IDAuMDQ0OGMwLjMwNzcgMCAwLjU0OTYtMC4wNTk3IDAuNzI1OS0wLjE3OTIgMC4xNzkyLTAuMTIyNSAwLjMwNjEtMC4yODY4IDAuMzgwOC0wLjQ5MjkgMC4wNzc3LTAuMjA2MSAwLjExNjUtMC40Mzc2IDAuMTE2NS0wLjY5NDV6bTMuMjY2NC0xLjc3NDN2My45MjVoLTEuMDc5OHYtNC44NDgxaDEuMDMwNmwwLjA0OTIgMC45MjMxem0xLjQ4MzItMC45NTQ0LTllLTMgMS4wMDM2Yy0wLjA2NTctMC4wMTE5LTAuMTM3NC0wLjAyMDktMC4yMTUxLTAuMDI2OC0wLjA3NDctNmUtMyAtMC4xNDkzLTllLTMgLTAuMjI0LTllLTMgLTAuMTg1MiAwLTAuMzQ4IDAuMDI2OS0wLjQ4ODQgMC4wODA3LTAuMTQwNCAwLjA1MDctMC4yNTg0IDAuMTI1NC0wLjM1NCAwLjIyNC0wLjA5MjYgMC4wOTU2LTAuMTY0MyAwLjIxMjEtMC4yMTUxIDAuMzQ5NS0wLjA1MDcgMC4xMzc0LTAuMDgwNiAwLjI5MTItMC4wODk2IDAuNDYxNWwtMC4yNDY0IDAuMDE3OWMwLTAuMzA0NyAwLjAyOTktMC41ODcgMC4wODk2LTAuODQ2OCAwLjA1OTctMC4yNTk5IDAuMTQ5NC0wLjQ4ODQgMC4yNjg4LTAuNjg1NiAwLjEyMjUtMC4xOTcxIDAuMjc0OS0wLjM1MSAwLjQ1NzEtMC40NjE1IDAuMTg1Mi0wLjExMDUgMC4zOTg4LTAuMTY1OCAwLjY0MDctMC4xNjU4IDAuMDY1NyAwIDAuMTM1OSA2ZS0zIDAuMjEwNiAwLjAxNzkgMC4wNzc3IDAuMDEyIDAuMTM1OSAwLjAyNTQgMC4xNzQ4IDAuMDQwNHptMi44Njc2IDQuOTY5MWMtMC4zNTg1IDAtMC42ODI2LTAuMDU4My0wLjk3MjMtMC4xNzQ4LTAuMjg2OC0wLjExOTUtMC41MzE3LTAuMjg1My0wLjczNDgtMC40OTczLTAuMjAwMi0wLjIxMjEtMC4zNTQtMC40NjE2LTAuNDYxNi0wLjc0ODMtMC4xMDc1LTAuMjg2OC0wLjE2MTMtMC41OTYtMC4xNjEzLTAuOTI3NXYtMC4xNzkzYzAtMC4zNzkzIDAuMDU1My0wLjcyMjggMC4xNjU4LTEuMDMwNSAwLjExMDYtMC4zMDc3IDAuMjY0NC0wLjU3MDYgMC40NjE1LTAuNzg4NiAwLjE5NzItMC4yMjExIDAuNDMwMi0wLjM4OTggMC42OTktMC41MDYzIDAuMjY4OS0wLjExNjUgMC41NjAxLTAuMTc0OCAwLjg3MzgtMC4xNzQ4IDAuMzQ2NSAwIDAuNjQ5NyAwLjA1ODMgMC45MDk1IDAuMTc0OCAwLjI1OTkgMC4xMTY1IDAuNDc1IDAuMjgwOCAwLjY0NTMgMC40OTI4IDAuMTcyOSAwLjIwOTEgMC4zMDE5IDAuNDU4NiAwLjM4NDkgMC43NDgzIDAuMDg3IDAuMjg5OCAwLjEzIDAuNjA5NCAwLjEzIDAuOTU4OXYwLjQ2MTVoLTMuNzQ1NXYtMC43NzUyaDIuNjc5NHYtMC4wODUxYy0wLjAwNTktMC4xOTQyLTAuMDQ0OC0wLjM3NjQtMC4xMTY1LTAuNTQ2Ni0wLjA2ODctMC4xNzAzLTAuMTc0Ny0wLjMwNzctMC4zMTgxLTAuNDEyMy0wLjE0MzQtMC4xMDQ1LTAuMzM0NS0wLjE1NjgtMC41NzM1LTAuMTU2OC0wLjE3OTIgMC0wLjMzOTEgMC4wMzg4LTAuNDc5NSAwLjExNjUtMC4xMzc0IDAuMDc0Ny0wLjI1MjQgMC4xODM3LTAuMzQ1IDAuMzI3MXMtMC4xNjQzIDAuMzE2Ni0wLjIxNSAwLjUxOThjLTAuMDQ3OCAwLjIwMDEtMC4wNzE3IDAuNDI1Ni0wLjA3MTcgMC42NzY1djAuMTc5M2MwIDAuMjEyMSAwLjAyODMgMC40MDkyIDAuMDg1MSAwLjU5MTQgMC4wNTk3IDAuMTc5MyAwLjE0NjQgMC4zMzYxIDAuMjU5OSAwLjQ3MDVzMC4yNTA5IDAuMjQwNSAwLjQxMjIgMC4zMTgxYzAuMTYxMyAwLjA3NDcgMC4zNDUgMC4xMTIgMC41NTExIDAuMTEyIDAuMjU5OSAwIDAuNDkxNC0wLjA1MjIgMC42OTQ1LTAuMTU2OCAwLjIwMzItMC4xMDQ1IDAuMzc5NC0wLjI1MjQgMC41Mjg4LTAuNDQzNmwwLjU2ODggMC41NTEyYy0wLjEwNCAwLjE1MjMtMC4yNCAwLjI5ODctMC40MDc1IDAuNDM5MS0wLjE2NzMgMC4xMzc0LTAuMzcxOSAwLjI0OTQtMC42MTM5IDAuMzM2LTAuMjM5IDAuMDg2Ni0wLjUxNjggMC4xMy0wLjgzMzQgMC4xM3oiIGZpbGwtb3BhY2l0eT0iLjg3Ii8+CiAgIDxwYXRoIGQ9Im01MC4zNTYgMzYuNTk2djAuNjY4N2gtMi40NTY2di0wLjY2ODdoMi40NTY2em0tMi4yMjEzLTQuMjI0MnY0Ljg5MjloLTAuODQzNXYtNC44OTI5aDAuODQzNXptNC45ODU5IDQuMTYzN3YtMS43MzRjMC0wLjEzLTAuMDIzNi0wLjI0Mi0wLjA3MDYtMC4zMzYxLTAuMDQ3MS0wLjA5NDEtMC4xMTg3LTAuMTY2OS0wLjIxNTEtMC4yMTg0LTAuMDk0MS0wLjA1MTUtMC4yMTI4LTAuMDc3My0wLjM1NjItMC4wNzczLTAuMTMyMiAwLTAuMjQ2NCAwLjAyMjQtMC4zNDI4IDAuMDY3Mi0wLjA5NjMgMC4wNDQ4LTAuMTcxNCAwLjEwNTMtMC4yMjUxIDAuMTgxNS0wLjA1MzggMC4wNzYyLTAuMDgwNyAwLjE2MjQtMC4wODA3IDAuMjU4N2gtMC44MDY1YzAtMC4xNDMzIDAuMDM0Ny0wLjI4MjIgMC4xMDQyLTAuNDE2NyAwLjA2OTQtMC4xMzQ0IDAuMTcwMi0wLjI1NDIgMC4zMDI0LTAuMzU5NXMwLjI5MDEtMC4xODgyIDAuNDczOS0wLjI0ODdjMC4xODM3LTAuMDYwNSAwLjM4OTgtMC4wOTA3IDAuNjE4My0wLjA5MDcgMC4yNzMzIDAgMC41MTUzIDAuMDQ1OSAwLjcyNTkgMC4xMzc3IDAuMjEyOCAwLjA5MTkgMC4zNzk3IDAuMjMwOCAwLjUwMDcgMC40MTY3IDAuMTIzMiAwLjE4MzcgMC4xODQ4IDAuNDE0NSAwLjE4NDggMC42OTIzdjEuNjE2NGMwIDAuMTY1OCAwLjAxMTIgMC4zMTQ4IDAuMDMzNiAwLjQ0NyAwLjAyNDcgMC4xMjk5IDAuMDU5NCAwLjI0MyAwLjEwNDIgMC4zMzk0djAuMDUzN2gtMC44MzAxYy0wLjAzOC0wLjA4NzMtMC4wNjgzLTAuMTk4Mi0wLjA5MDctMC4zMzI2LTAuMDIwMi0wLjEzNjctMC4wMzAyLTAuMjY4OS0wLjAzMDItMC4zOTY2em0wLjExNzYtMS40ODIgMC4wMDY3IDAuNTAwN2gtMC41ODE0Yy0wLjE1MDEgMC0wLjI4MjMgMC4wMTQ2LTAuMzk2NSAwLjA0MzctMC4xMTQzIDAuMDI2OS0wLjIwOTUgMC4wNjcyLTAuMjg1NyAwLjEyMS0wLjA3NjEgMC4wNTM4LTAuMTMzMyAwLjExODctMC4xNzEzIDAuMTk0OS0wLjAzODEgMC4wNzYyLTAuMDU3MiAwLjE2MjQtMC4wNTcyIDAuMjU4OCAwIDAuMDk2MyAwLjAyMjQgMC4xODQ4IDAuMDY3MiAwLjI2NTUgMC4wNDQ4IDAuMDc4NCAwLjEwOTggMC4xNCAwLjE5NDkgMC4xODQ4IDAuMDg3NCAwLjA0NDggMC4xOTI3IDAuMDY3MiAwLjMxNTkgMC4wNjcyIDAuMTY1OCAwIDAuMzEwMy0wLjAzMzYgMC40MzM1LTAuMTAwOCAwLjEyNTUtMC4wNjk1IDAuMjI0MS0wLjE1MzUgMC4yOTU4LTAuMjUyMSAwLjA3MTctMC4xMDA4IDAuMTA5Ny0wLjE5NiAwLjExNDItMC4yODU2bDAuMjYyMiAwLjM1OTZjLTAuMDI2OSAwLjA5MTgtMC4wNzI5IDAuMTkwNC0wLjEzNzggMC4yOTU3LTAuMDY1IDAuMTA1My0wLjE1MDEgMC4yMDYxLTAuMjU1NCAwLjMwMjQtMC4xMDMxIDAuMDk0MS0wLjIyNzQgMC4xNzE0LTAuMzczIDAuMjMxOS0wLjE0MzQgMC4wNjA1LTAuMzA5MiAwLjA5MDgtMC40OTc0IDAuMDkwOC0wLjIzNzUgMC0wLjQ0OTItMC4wNDcxLTAuNjM1MS0wLjE0MTItMC4xODYtMC4wOTYzLTAuMzMxNi0wLjIyNTEtMC40MzY5LTAuMzg2NC0wLjEwNTMtMC4xNjM2LTAuMTU4LTAuMzQ4NC0wLjE1OC0wLjU1NDUgMC0wLjE5MjcgMC4wMzU5LTAuMzYzIDAuMTA3Ni0wLjUxMDggMC4wNzM5LTAuMTUwMSAwLjE4MTQtMC4yNzU2IDAuMzIyNi0wLjM3NjQgMC4xNDM0LTAuMTAwOCAwLjMxODEtMC4xNzcgMC41MjQyLTAuMjI4NSAwLjIwNjEtMC4wNTM4IDAuNDQxNC0wLjA4MDcgMC43MDU3LTAuMDgwN2gwLjYzNTJ6bTMuNzI1NyAxLjIyNjZjMC0wLjA4MDYtMC4wMjAyLTAuMTUzNC0wLjA2MDUtMC4yMTg0LTAuMDQwMy0wLjA2NzItMC4xMTc2LTAuMTI3Ny0wLjIzMTktMC4xODE1LTAuMTEyLTAuMDUzOC0wLjI3NzgtMC4xMDMtMC40OTczLTAuMTQ3OS0wLjE5MjctMC4wNDI1LTAuMzY5Ny0wLjA5MjktMC41MzEtMC4xNTEyLTAuMTU5MS0wLjA2MDUtMC4yOTU3LTAuMTMzMy0wLjQxLTAuMjE4NC0wLjExNDItMC4wODUxLTAuMjAyNy0wLjE4Ni0wLjI2NTUtMC4zMDI1LTAuMDYyNy0wLjExNjUtMC4wOTQxLTAuMjUwOS0wLjA5NDEtMC40MDMyIDAtMC4xNDc5IDAuMDMyNS0wLjI4NzkgMC4wOTc1LTAuNDIwMXMwLjE1NzktMC4yNDg3IDAuMjc4OS0wLjM0OTUgMC4yNjc3LTAuMTgwMyAwLjQ0MDItMC4yMzg2YzAuMTc0OC0wLjA1ODIgMC4zNjk3LTAuMDg3MyAwLjU4NDgtMC4wODczIDAuMzA0NyAwIDAuNTY1NyAwLjA1MTUgMC43ODMgMC4xNTQ1IDAuMjE5NSAwLjEwMDkgMC4zODc2IDAuMjM4NiAwLjUwNDEgMC40MTM0IDAuMTE2NSAwLjE3MjUgMC4xNzQ3IDAuMzY3NCAwLjE3NDcgMC41ODQ3aC0wLjgwOTljMC0wLjA5NjMtMC4wMjQ2LTAuMTg1OS0wLjA3MzktMC4yNjg4LTAuMDQ3MS0wLjA4NTItMC4xMTg4LTAuMTUzNS0wLjIxNTEtMC4yMDUtMC4wOTYzLTAuMDUzOC0wLjIxNzMtMC4wODA3LTAuMzYyOS0wLjA4MDctMC4xMzg5IDAtMC4yNTQzIDAuMDIyNC0wLjM0NjIgMC4wNjcyLTAuMDg5NiAwLjA0MjYtMC4xNTY4IDAuMDk4Ni0wLjIwMTYgMC4xNjgxLTAuMDQyNiAwLjA2OTQtMC4wNjM4IDAuMTQ1Ni0wLjA2MzggMC4yMjg1IDAgMC4wNjA1IDAuMDExMiAwLjExNTQgMC4wMzM2IDAuMTY0NiAwLjAyNDYgMC4wNDcxIDAuMDY0OSAwLjA5MDggMC4xMjA5IDAuMTMxMSAwLjA1NjEgMC4wMzgxIDAuMTMyMiAwLjA3MzkgMC4yMjg2IDAuMTA3NSAwLjA5ODUgMC4wMzM2IDAuMjIxOCAwLjA2NjEgMC4zNjk2IDAuMDk3NSAwLjI3NzggMC4wNTgyIDAuNTE2NCAwLjEzMzMgMC43MTU4IDAuMjI1MSAwLjIwMTYgMC4wODk3IDAuMzU2MiAwLjIwNjIgMC40NjM4IDAuMzQ5NSAwLjEwNzUgMC4xNDEyIDAuMTYxMyAwLjMyMDQgMC4xNjEzIDAuNTM3NyAwIDAuMTYxMy0wLjAzNDggMC4zMDkyLTAuMTA0MiAwLjQ0MzYtMC4wNjcyIDAuMTMyMi0wLjE2NTggMC4yNDc2LTAuMjk1NyAwLjM0NjItMC4xMyAwLjA5NjMtMC4yODU3IDAuMTcxMy0wLjQ2NzIgMC4yMjUxLTAuMTc5MiAwLjA1MzgtMC4zODA4IDAuMDgwNy0wLjYwNDggMC4wODA3LTAuMzI5NCAwLTAuNjA4My0wLjA1ODMtMC44MzY4LTAuMTc0OC0wLjIyODUtMC4xMTg3LTAuNDAyMi0wLjI3LTAuNTIwOS0wLjQ1MzctMC4xMTY1LTAuMTg1OS0wLjE3NDctMC4zNzg2LTAuMTc0Ny0wLjU3OGgwLjc4M2MwLjAwODkgMC4xNTAxIDAuMDUwNCAwLjI3IDAuMTI0MyAwLjM1OTYgMC4wNzYyIDAuMDg3NCAwLjE3MDMgMC4xNTEyIDAuMjgyMyAwLjE5MTYgMC4xMTQyIDAuMDM4IDAuMjMxOSAwLjA1NzEgMC4zNTI4IDAuMDU3MSAwLjE0NTcgMCAwLjI2NzgtMC4wMTkxIDAuMzY2My0wLjA1NzEgMC4wOTg2LTAuMDQwNCAwLjE3MzctMC4wOTQxIDAuMjI1Mi0wLjE2MTMgMC4wNTE1LTAuMDY5NSAwLjA3NzMtMC4xNDc5IDAuMDc3My0wLjIzNTN6bTMuMzEyMy0yLjY1MTR2MC41OTE0aC0yLjA0OTl2LTAuNTkxNGgyLjA0OTl6bS0xLjQ1ODQtMC44OTA2aDAuODA5OXYzLjUyMTljMCAwLjExMiAwLjAxNTYgMC4xOTgyIDAuMDQ3IDAuMjU4NyAwLjAzMzYgMC4wNTgzIDAuMDc5NSAwLjA5NzUgMC4xMzc4IDAuMTE3NiAwLjA1ODIgMC4wMjAyIDAuMTI2NiAwLjAzMDMgMC4yMDUgMC4wMzAzIDAuMDU2IDAgMC4xMDk4LTAuMDAzNCAwLjE2MTMtMC4wMTAxczAuMDkzLTAuMDEzNCAwLjEyNDMtMC4wMjAybDAuMDAzNCAwLjYxODRjLTAuMDY3MiAwLjAyMDEtMC4xNDU2IDAuMDM4MS0wLjIzNTMgMC4wNTM3LTAuMDg3MyAwLjAxNTctMC4xODgxIDAuMDIzNi0wLjMwMjQgMC4wMjM2LTAuMTg2IDAtMC4zNTA2LTAuMDMyNS0wLjQ5NC0wLjA5NzUtMC4xNDM0LTAuMDY3Mi0wLjI1NTQtMC4xNzU5LTAuMzM2MS0wLjMyNi0wLjA4MDYtMC4xNTAxLTAuMTIwOS0wLjM0OTUtMC4xMjA5LTAuNTk4MXYtMy41NzIzem02LjI3MTggMy42Njk3di0yLjc3OTFoMC44MTMzdjMuNjM2aC0wLjc2NjJsLTAuMDQ3MS0wLjg1Njl6bTAuMTE0My0wLjc1NjEgMC4yNzIyLTAuMDA2N2MwIDAuMjQ0Mi0wLjAyNjkgMC40NjkzLTAuMDgwNyAwLjY3NTQtMC4wNTM3IDAuMjAzOS0wLjEzNjYgMC4zODItMC4yNDg2IDAuNTM0NC0wLjExMjEgMC4xNTAxLTAuMjU1NCAwLjI2NzctMC40MzAyIDAuMzUyOC0wLjE3NDcgMC4wODI5LTAuMzg0MiAwLjEyNDQtMC42Mjg0IDAuMTI0NC0wLjE3NyAwLTAuMzM5NC0wLjAyNTgtMC40ODczLTAuMDc3My0wLjE0NzgtMC4wNTE2LTAuMjc1NS0wLjEzMTEtMC4zODMxLTAuMjM4Ni0wLjEwNTMtMC4xMDc2LTAuMTg3MS0wLjI0NzYtMC4yNDUzLTAuNDIwMS0wLjA1ODMtMC4xNzI1LTAuMDg3NC0wLjM3ODYtMC4wODc0LTAuNjE4M3YtMi4zNDloMC44MDk5djIuMzU1N2MwIDAuMTMyMiAwLjAxNTcgMC4yNDMxIDAuMDQ3MSAwLjMzMjcgMC4wMzEzIDAuMDg3NCAwLjA3MzkgMC4xNTc5IDAuMTI3NyAwLjIxMTcgMC4wNTM3IDAuMDUzOCAwLjExNjUgMC4wOTE4IDAuMTg4MSAwLjExNDMgMC4wNzE3IDAuMDIyNCAwLjE0NzkgMC4wMzM2IDAuMjI4NiAwLjAzMzYgMC4yMzA3IDAgMC40MTIyLTAuMDQ0OSAwLjU0NDQtMC4xMzQ1IDAuMTM0NC0wLjA5MTggMC4yMjk2LTAuMjE1IDAuMjg1Ni0wLjM2OTYgMC4wNTgzLTAuMTU0NiAwLjA4NzQtMC4zMjgyIDAuMDg3NC0wLjUyMDl6bTIuNDg1Ny0xLjMyNHY0LjMzNWgtMC44MDk5di01LjAzNGgwLjc0NmwwLjA2MzkgMC42OTl6bTIuMzY5MSAxLjA4NTR2MC4wNzA2YzAgMC4yNjQzLTAuMDMxMyAwLjUwOTctMC4wOTQxIDAuNzM1OS0wLjA2MDUgMC4yMjQxLTAuMTUxMiAwLjQyMDEtMC4yNzIyIDAuNTg4MS0wLjExODcgMC4xNjU4LTAuMjY1NSAwLjI5NDYtMC40NDAyIDAuMzg2NS0wLjE3NDggMC4wOTE4LTAuMzc2NCAwLjEzNzgtMC42MDQ5IDAuMTM3OC0wLjIyNjMgMC0wLjQyNDUtMC4wNDE1LTAuNTk0OC0wLjEyNDQtMC4xNjgtMC4wODUxLTAuMzEwMy0wLjIwNS0wLjQyNjgtMC4zNTk2LTAuMTE2NS0wLjE1NDUtMC4yMTA2LTAuMzM2LTAuMjgyMy0wLjU0NDQtMC4wNjk0LTAuMjEwNi0wLjExODctMC40NDEzLTAuMTQ3OC0wLjY5MjJ2LTAuMjcyMmMwLjAyOTEtMC4yNjY2IDAuMDc4NC0wLjUwODYgMC4xNDc4LTAuNzI1OSAwLjA3MTctMC4yMTczIDAuMTY1OC0wLjQwNDQgMC4yODIzLTAuNTYxMnMwLjI1ODgtMC4yNzc4IDAuNDI2OC0wLjM2MjljMC4xNjgtMC4wODUyIDAuMzY0LTAuMTI3NyAwLjU4ODEtMC4xMjc3IDAuMjI4NSAwIDAuNDMxMiAwLjA0NDggMC42MDgyIDAuMTM0NCAwLjE3NyAwLjA4NzMgMC4zMjYgMC4yMTI4IDAuNDQ3IDAuMzc2NCAwLjEyMSAwLjE2MTMgMC4yMTE3IDAuMzU2MiAwLjI3MjIgMC41ODQ3IDAuMDYwNSAwLjIyNjMgMC4wOTA3IDAuNDc4MyAwLjA5MDcgMC43NTYxem0tMC44MDk5IDAuMDcwNnYtMC4wNzA2YzAtMC4xNjgtMC4wMTU2LTAuMzIzNy0wLjA0Ny0wLjQ2NzEtMC4wMzE0LTAuMTQ1Ni0wLjA4MDctMC4yNzMzLTAuMTQ3OS0wLjM4MzFzLTAuMTUzNC0wLjE5NDktMC4yNTg3LTAuMjU1NGMtMC4xMDMxLTAuMDYyNy0wLjIyNzQtMC4wOTQxLTAuMzczMS0wLjA5NDEtMC4xNDMzIDAtMC4yNjY2IDAuMDI0Ni0wLjM2OTYgMC4wNzM5LTAuMTAzMSAwLjA0NzEtMC4xODkzIDAuMTEzMi0wLjI1ODggMC4xOTgzLTAuMDY5NCAwLjA4NTEtMC4xMjMyIDAuMTg0OC0wLjE2MTMgMC4yOTkxLTAuMDM4MSAwLjExMi0wLjA2NDkgMC4yMzQxLTAuMDgwNiAwLjM2NjN2MC42NTE5YzAuMDI2OSAwLjE2MTMgMC4wNzI4IDAuMzA5MiAwLjEzNzggMC40NDM2IDAuMDY0OSAwLjEzNDQgMC4xNTY4IDAuMjQyIDAuMjc1NSAwLjMyMjYgMC4xMjEgMC4wNzg0IDAuMjc1NiAwLjExNzYgMC40NjM4IDAuMTE3NiAwLjE0NTYgMCAwLjI2OTktMC4wMzEzIDAuMzczLTAuMDk0MSAwLjEwMy0wLjA2MjcgMC4xODcxLTAuMTQ4OSAwLjI1Mi0wLjI1ODcgMC4wNjcyLTAuMTEyIDAuMTE2NS0wLjI0MDkgMC4xNDc5LTAuMzg2NXMwLjA0Ny0wLjMwMDIgMC4wNDctMC40NjM3em0zLjg2MDIgMS4wMjgzdi00LjQwOWgwLjgxMzJ2NS4xNjE3aC0wLjczNTlsLTAuMDc3My0wLjc1Mjd6bS0yLjM2NTktMS4wMjV2LTAuMDcwNWMwLTAuMjc1NiAwLjAzMjUtMC41MjY1IDAuMDk3NS0wLjc1MjggMC4wNjUtMC4yMjg1IDAuMTU5MS0wLjQyNDUgMC4yODIzLTAuNTg4MSAwLjEyMzItMC4xNjU4IDAuMjczMy0wLjI5MjQgMC40NTAzLTAuMzc5NyAwLjE3Ny0wLjA4OTYgMC4zNzY0LTAuMTM0NCAwLjU5ODItMC4xMzQ0IDAuMjE5NSAwIDAuNDEyMiAwLjA0MjUgMC41NzggMC4xMjc3IDAuMTY1OCAwLjA4NTEgMC4zMDY5IDAuMjA3MiAwLjQyMzQgMC4zNjYyIDAuMTE2NSAwLjE1NjkgMC4yMDk1IDAuMzQ1MSAwLjI3ODkgMC41NjQ2IDAuMDY5NSAwLjIxNzMgMC4xMTg4IDAuNDU5MyAwLjE0NzkgMC43MjU5djAuMjI1MWMtMC4wMjkxIDAuMjU5OS0wLjA3ODQgMC40OTc0LTAuMTQ3OSAwLjcxMjUtMC4wNjk0IDAuMjE1LTAuMTYyNCAwLjQwMS0wLjI3ODkgMC41NTc4cy0wLjI1ODggMC4yNzc4LTAuNDI2OCAwLjM2M2MtMC4xNjU4IDAuMDg1MS0wLjM1OTYgMC4xMjc3LTAuNTgxMyAwLjEyNzctMC4yMTk2IDAtMC40MTc5LTAuMDQ2LTAuNTk0OS0wLjEzNzgtMC4xNzQ3LTAuMDkxOS0wLjMyMzctMC4yMjA3LTAuNDQ2OS0wLjM4NjVzLTAuMjE3My0wLjM2MDctMC4yODIzLTAuNTg0N2MtMC4wNjUtMC4yMjYzLTAuMDk3NS0wLjQ3MTYtMC4wOTc1LTAuNzM2em0wLjgwOTktMC4wNzA1djAuMDcwNWMwIDAuMTY1OCAwLjAxNDYgMC4zMjA0IDAuMDQzNyAwLjQ2MzggMC4wMzE0IDAuMTQzNCAwLjA3OTYgMC4yNjk5IDAuMTQ0NSAwLjM3OTcgMC4wNjUgMC4xMDc2IDAuMTQ5IDAuMTkyNyAwLjI1MjEgMC4yNTU0IDAuMTA1MyAwLjA2MDUgMC4yMzA3IDAuMDkwOCAwLjM3NjMgMC4wOTA4IDAuMTgzOCAwIDAuMzM1LTAuMDQwNCAwLjQ1MzctMC4xMjEgMC4xMTg4LTAuMDgwNyAwLjIxMTctMC4xODkzIDAuMjc4OS0wLjMyNiAwLjA2OTUtMC4xMzg5IDAuMTE2NS0wLjI5MzUgMC4xNDEyLTAuNDYzN3YtMC42MDgzYy0wLjAxMzUtMC4xMzIyLTAuMDQxNS0wLjI1NTQtMC4wODQtMC4zNjk3LTAuMDQwNC0wLjExNDItMC4wOTUyLTAuMjEzOS0wLjE2NDctMC4yOTktMC4wNjk1LTAuMDg3NC0wLjE1NTctMC4xNTQ2LTAuMjU4OC0wLjIwMTctMC4xMDA4LTAuMDQ5My0wLjIyMDYtMC4wNzM5LTAuMzU5NS0wLjA3MzktMC4xNDc5IDAtMC4yNzM0IDAuMDMxNC0wLjM3NjQgMC4wOTQxLTAuMTAzMSAwLjA2MjctMC4xODgyIDAuMTQ5LTAuMjU1NCAwLjI1ODctMC4wNjUgMC4xMDk4LTAuMTEzMiAwLjIzNzUtMC4xNDQ1IDAuMzgzMS0wLjAzMTQgMC4xNDU3LTAuMDQ3MSAwLjMwMTQtMC4wNDcxIDAuNDY3MnptNS40MDk0IDEuMTE5di0xLjczNGMwLTAuMTMtMC4wMjM2LTAuMjQyLTAuMDcwNi0wLjMzNjEtMC4wNDcxLTAuMDk0MS0wLjExODgtMC4xNjY5LTAuMjE1MS0wLjIxODQtMC4wOTQxLTAuMDUxNS0wLjIxMjgtMC4wNzczLTAuMzU2Mi0wLjA3NzMtMC4xMzIyIDAtMC4yNDY0IDAuMDIyNC0wLjM0MjggMC4wNjcyLTAuMDk2MyAwLjA0NDgtMC4xNzE0IDAuMTA1My0wLjIyNTEgMC4xODE1LTAuMDUzOCAwLjA3NjItMC4wODA3IDAuMTYyNC0wLjA4MDcgMC4yNTg3aC0wLjgwNjVjMC0wLjE0MzMgMC4wMzQ3LTAuMjgyMiAwLjEwNDItMC40MTY3IDAuMDY5NC0wLjEzNDQgMC4xNzAyLTAuMjU0MiAwLjMwMjQtMC4zNTk1czAuMjkwMS0wLjE4ODIgMC40NzM4LTAuMjQ4N2MwLjE4MzgtMC4wNjA1IDAuMzg5OS0wLjA5MDcgMC42MTg0LTAuMDkwNyAwLjI3MzMgMCAwLjUxNTMgMC4wNDU5IDAuNzI1OSAwLjEzNzcgMC4yMTI4IDAuMDkxOSAwLjM3OTcgMC4yMzA4IDAuNTAwNyAwLjQxNjcgMC4xMjMyIDAuMTgzNyAwLjE4NDggMC40MTQ1IDAuMTg0OCAwLjY5MjN2MS42MTY0YzAgMC4xNjU4IDAuMDExMiAwLjMxNDggMC4wMzM2IDAuNDQ3IDAuMDI0NyAwLjEyOTkgMC4wNTk0IDAuMjQzIDAuMTA0MiAwLjMzOTR2MC4wNTM3aC0wLjgzMDFjLTAuMDM4LTAuMDg3My0wLjA2ODMtMC4xOTgyLTAuMDkwNy0wLjMzMjYtMC4wMjAyLTAuMTM2Ny0wLjAzMDItMC4yNjg5LTAuMDMwMi0wLjM5NjZ6bTAuMTE3Ni0xLjQ4MiAwLjAwNjcgMC41MDA3aC0wLjU4MTRjLTAuMTUwMSAwLTAuMjgyMyAwLjAxNDYtMC4zOTY1IDAuMDQzNy0wLjExNDMgMC4wMjY5LTAuMjA5NSAwLjA2NzItMC4yODU3IDAuMTIxLTAuMDc2MSAwLjA1MzgtMC4xMzMzIDAuMTE4Ny0wLjE3MTMgMC4xOTQ5LTAuMDM4MSAwLjA3NjItMC4wNTcyIDAuMTYyNC0wLjA1NzIgMC4yNTg4IDAgMC4wOTYzIDAuMDIyNCAwLjE4NDggMC4wNjcyIDAuMjY1NSAwLjA0NDggMC4wNzg0IDAuMTA5OCAwLjE0IDAuMTk0OSAwLjE4NDggMC4wODc0IDAuMDQ0OCAwLjE5MjcgMC4wNjcyIDAuMzE1OSAwLjA2NzIgMC4xNjU4IDAgMC4zMTAzLTAuMDMzNiAwLjQzMzUtMC4xMDA4IDAuMTI1NS0wLjA2OTUgMC4yMjQxLTAuMTUzNSAwLjI5NTgtMC4yNTIxIDAuMDcxNy0wLjEwMDggMC4xMDk3LTAuMTk2IDAuMTE0Mi0wLjI4NTZsMC4yNjIxIDAuMzU5NmMtMC4wMjY4IDAuMDkxOC0wLjA3MjggMC4xOTA0LTAuMTM3NyAwLjI5NTctMC4wNjUgMC4xMDUzLTAuMTUwMSAwLjIwNjEtMC4yNTU0IDAuMzAyNC0wLjEwMzEgMC4wOTQxLTAuMjI3NCAwLjE3MTQtMC4zNzMxIDAuMjMxOS0wLjE0MzMgMC4wNjA1LTAuMzA5MSAwLjA5MDgtMC40OTczIDAuMDkwOC0wLjIzNzUgMC0wLjQ0OTItMC4wNDcxLTAuNjM1MS0wLjE0MTItMC4xODYtMC4wOTYzLTAuMzMxNi0wLjIyNTEtMC40MzY5LTAuMzg2NC0wLjEwNTMtMC4xNjM2LTAuMTU4LTAuMzQ4NC0wLjE1OC0wLjU1NDUgMC0wLjE5MjcgMC4wMzU5LTAuMzYzIDAuMTA3Ni0wLjUxMDggMC4wNzM5LTAuMTUwMSAwLjE4MTQtMC4yNzU2IDAuMzIyNi0wLjM3NjQgMC4xNDM0LTAuMTAwOCAwLjMxODEtMC4xNzcgMC41MjQyLTAuMjI4NSAwLjIwNjEtMC4wNTM4IDAuNDQxNC0wLjA4MDcgMC43MDU3LTAuMDgwN2gwLjYzNTJ6bTMuMzUyNy0xLjQyNDh2MC41OTE0aC0yLjA1di0wLjU5MTRoMi4wNXptLTEuNDU4NS0wLjg5MDZoMC44MDk5djMuNTIxOWMwIDAuMTEyIDAuMDE1NyAwLjE5ODIgMC4wNDcgMC4yNTg3IDAuMDMzNiAwLjA1ODMgMC4wNzk2IDAuMDk3NSAwLjEzNzggMC4xMTc2IDAuMDU4MyAwLjAyMDIgMC4xMjY2IDAuMDMwMyAwLjIwNSAwLjAzMDMgMC4wNTYgMCAwLjEwOTgtMC4wMDM0IDAuMTYxMy0wLjAxMDFzMC4wOTMtMC4wMTM0IDAuMTI0My0wLjAyMDJsMC4wMDM0IDAuNjE4NGMtMC4wNjcyIDAuMDIwMS0wLjE0NTYgMC4wMzgxLTAuMjM1MiAwLjA1MzctMC4wODc0IDAuMDE1Ny0wLjE4ODIgMC4wMjM2LTAuMzAyNSAwLjAyMzYtMC4xODU5IDAtMC4zNTA2LTAuMDMyNS0wLjQ5NC0wLjA5NzUtMC4xNDM0LTAuMDY3Mi0wLjI1NTQtMC4xNzU5LTAuMzM2LTAuMzI2LTAuMDgwNy0wLjE1MDEtMC4xMjEtMC4zNDk1LTAuMTIxLTAuNTk4MXYtMy41NzIzem0zLjgyOTkgNC41OTM5Yy0wLjI2ODkgMC0wLjUxMi0wLjA0MzctMC43MjkzLTAuMTMxMS0wLjIxNS0wLjA4OTYtMC4zOTg3LTAuMjE0LTAuNTUxMS0wLjM3My0wLjE1MDEtMC4xNTkxLTAuMjY1NS0wLjM0NjItMC4zNDYxLTAuNTYxMi0wLjA4MDctMC4yMTUxLTAuMTIxLTAuNDQ3LTAuMTIxLTAuNjk1N3YtMC4xMzQ0YzAtMC4yODQ1IDAuMDQxNC0wLjU0MjEgMC4xMjQzLTAuNzcyOXMwLjE5ODMtMC40Mjc5IDAuMzQ2Mi0wLjU5MTRjMC4xNDc4LTAuMTY1OCAwLjMyMjYtMC4yOTI0IDAuNTI0Mi0wLjM3OThzMC40MjAxLTAuMTMxIDAuNjU1My0wLjEzMWMwLjI1OTkgMCAwLjQ4NzMgMC4wNDM2IDAuNjgyMiAwLjEzMXMwLjM1NjIgMC4yMTA2IDAuNDgzOSAwLjM2OTdjMC4xMyAwLjE1NjggMC4yMjYzIDAuMzQzOSAwLjI4OSAwLjU2MTIgMC4wNjUgMC4yMTczIDAuMDk3NSAwLjQ1NyAwLjA5NzUgMC43MTkxdjAuMzQ2MmgtMi44MDk0di0wLjU4MTRoMi4wMDk2di0wLjA2MzljLTAuMDA0NS0wLjE0NTYtMC4wMzM2LTAuMjgyMi0wLjA4NzQtMC40MDk5LTAuMDUxNS0wLjEyNzctMC4xMzExLTAuMjMwOC0wLjIzODYtMC4zMDkycy0wLjI1MDktMC4xMTc2LTAuNDMwMS0wLjExNzZjLTAuMTM0NSAwLTAuMjU0MyAwLjAyOTEtMC4zNTk2IDAuMDg3My0wLjEwMzEgMC4wNTYxLTAuMTg5MyAwLjEzNzgtMC4yNTg4IDAuMjQ1NC0wLjA2OTQgMC4xMDc1LTAuMTIzMiAwLjIzNzQtMC4xNjEzIDAuMzg5OC0wLjAzNTggMC4xNTAxLTAuMDUzOCAwLjMxOTItMC4wNTM4IDAuNTA3NHYwLjEzNDRjMCAwLjE1OTEgMC4wMjEzIDAuMzA3IDAuMDYzOSAwLjQ0MzYgMC4wNDQ4IDAuMTM0NSAwLjEwOTggMC4yNTIxIDAuMTk0OSAwLjM1MjlzMC4xODgyIDAuMTgwMyAwLjMwOTIgMC4yMzg2YzAuMTIwOSAwLjA1NiAwLjI1ODcgMC4wODQgMC40MTMzIDAuMDg0IDAuMTk0OSAwIDAuMzY4Ni0wLjAzOTIgMC41MjA5LTAuMTE3NnMwLjI4NDUtMC4xODkzIDAuMzk2NS0wLjMzMjdsMC40MjY4IDAuNDEzM2MtMC4wNzg0IDAuMTE0My0wLjE4MDMgMC4yMjQxLTAuMzA1OCAwLjMyOTQtMC4xMjU0IDAuMTAzLTAuMjc4OSAwLjE4Ny0wLjQ2MDQgMC4yNTItMC4xNzkyIDAuMDY1LTAuMzg3NiAwLjA5NzUtMC42MjUgMC4wOTc1em02LjI1MTctNC45Nzd2NC45MDk3aC0wLjgwOTl2LTMuOTQ4NmwtMS4xOTk3IDAuNDA2N3YtMC42Njg4bDEuOTEyMS0wLjY5OWgwLjA5NzV6bTQuMTA4OCA0LjE1N3YtNC40MDloMC44MTMydjUuMTYxN2gtMC43MzU5bC0wLjA3NzMtMC43NTI3em0tMi4zNjU4LTEuMDI1di0wLjA3MDVjMC0wLjI3NTYgMC4wMzI0LTAuNTI2NSAwLjA5NzQtMC43NTI4IDAuMDY1LTAuMjI4NSAwLjE1OTEtMC40MjQ1IDAuMjgyMy0wLjU4ODEgMC4xMjMyLTAuMTY1OCAwLjI3MzMtMC4yOTI0IDAuNDUwMy0wLjM3OTcgMC4xNzctMC4wODk2IDAuMzc2NC0wLjEzNDQgMC41OTgyLTAuMTM0NCAwLjIxOTUgMCAwLjQxMjIgMC4wNDI1IDAuNTc4IDAuMTI3NyAwLjE2NTggMC4wODUxIDAuMzA2OSAwLjIwNzIgMC40MjM0IDAuMzY2MiAwLjExNjUgMC4xNTY5IDAuMjA5NSAwLjM0NTEgMC4yNzg5IDAuNTY0NiAwLjA2OTUgMC4yMTczIDAuMTE4OCAwLjQ1OTMgMC4xNDc5IDAuNzI1OXYwLjIyNTFjLTAuMDI5MSAwLjI1OTktMC4wNzg0IDAuNDk3NC0wLjE0NzkgMC43MTI1LTAuMDY5NCAwLjIxNS0wLjE2MjQgMC40MDEtMC4yNzg5IDAuNTU3OHMtMC4yNTg3IDAuMjc3OC0wLjQyNjggMC4zNjNjLTAuMTY1OCAwLjA4NTEtMC4zNTk1IDAuMTI3Ny0wLjU4MTMgMC4xMjc3LTAuMjE5NiAwLTAuNDE3OS0wLjA0Ni0wLjU5NDktMC4xMzc4LTAuMTc0Ny0wLjA5MTktMC4zMjM3LTAuMjIwNy0wLjQ0NjktMC4zODY1cy0wLjIxNzMtMC4zNjA3LTAuMjgyMy0wLjU4NDdjLTAuMDY1LTAuMjI2My0wLjA5NzQtMC40NzE2LTAuMDk3NC0wLjczNnptMC44MDk4LTAuMDcwNXYwLjA3MDVjMCAwLjE2NTggMC4wMTQ2IDAuMzIwNCAwLjA0MzcgMC40NjM4IDAuMDMxNCAwLjE0MzQgMC4wNzk2IDAuMjY5OSAwLjE0NDUgMC4zNzk3IDAuMDY1IDAuMTA3NiAwLjE0OSAwLjE5MjcgMC4yNTIxIDAuMjU1NCAwLjEwNTMgMC4wNjA1IDAuMjMwNyAwLjA5MDggMC4zNzYzIDAuMDkwOCAwLjE4MzggMCAwLjMzNS0wLjA0MDQgMC40NTM3LTAuMTIxIDAuMTE4OC0wLjA4MDcgMC4yMTE3LTAuMTg5MyAwLjI3ODktMC4zMjYgMC4wNjk1LTAuMTM4OSAwLjExNjUtMC4yOTM1IDAuMTQxMi0wLjQ2Mzd2LTAuNjA4M2MtMC4wMTM1LTAuMTMyMi0wLjA0MTUtMC4yNTU0LTAuMDg0LTAuMzY5Ny0wLjA0MDQtMC4xMTQyLTAuMDk1Mi0wLjIxMzktMC4xNjQ3LTAuMjk5LTAuMDY5NC0wLjA4NzQtMC4xNTU3LTAuMTU0Ni0wLjI1ODgtMC4yMDE3LTAuMTAwOC0wLjA0OTMtMC4yMjA2LTAuMDczOS0wLjM1OTUtMC4wNzM5LTAuMTQ3OSAwLTAuMjczNCAwLjAzMTQtMC4zNzY0IDAuMDk0MS0wLjEwMzEgMC4wNjI3LTAuMTg4MiAwLjE0OS0wLjI1NTQgMC4yNTg3LTAuMDY1IDAuMTA5OC0wLjExMzEgMC4yMzc1LTAuMTQ0NSAwLjM4MzEtMC4wMzE0IDAuMTQ1Ny0wLjA0NzEgMC4zMDE0LTAuMDQ3MSAwLjQ2NzJ6bTcuMjY2NiAxLjExOXYtMS43MzRjMC0wLjEzLTAuMDIzNS0wLjI0Mi0wLjA3MDYtMC4zMzYxLTAuMDQ3LTAuMDk0MS0wLjExODctMC4xNjY5LTAuMjE1LTAuMjE4NC0wLjA5NDEtMC4wNTE1LTAuMjEyOS0wLjA3NzMtMC4zNTYyLTAuMDc3My0wLjEzMjIgMC0wLjI0NjUgMC4wMjI0LTAuMzQyOCAwLjA2NzItMC4wOTY0IDAuMDQ0OC0wLjE3MTQgMC4xMDUzLTAuMjI1MiAwLjE4MTUtMC4wNTM3IDAuMDc2Mi0wLjA4MDYgMC4xNjI0LTAuMDgwNiAwLjI1ODdoLTAuODA2NmMwLTAuMTQzMyAwLjAzNDgtMC4yODIyIDAuMTA0Mi0wLjQxNjcgMC4wNjk1LTAuMTM0NCAwLjE3MDMtMC4yNTQyIDAuMzAyNS0wLjM1OTUgMC4xMzIxLTAuMTA1MyAwLjI5MDEtMC4xODgyIDAuNDczOC0wLjI0ODdzMC4zODk4LTAuMDkwNyAwLjYxODMtMC4wOTA3YzAuMjczNCAwIDAuNTE1MyAwLjA0NTkgMC43MjU5IDAuMTM3NyAwLjIxMjggMC4wOTE5IDAuMzc5OCAwLjIzMDggMC41MDA3IDAuNDE2NyAwLjEyMzIgMC4xODM3IDAuMTg0OSAwLjQxNDUgMC4xODQ5IDAuNjkyM3YxLjYxNjRjMCAwLjE2NTggMC4wMTEyIDAuMzE0OCAwLjAzMzYgMC40NDcgMC4wMjQ2IDAuMTI5OSAwLjA1OTMgMC4yNDMgMC4xMDQxIDAuMzM5NHYwLjA1MzdoLTAuODNjLTAuMDM4MS0wLjA4NzMtMC4wNjgzLTAuMTk4Mi0wLjA5MDctMC4zMzI2LTAuMDIwMi0wLjEzNjctMC4wMzAzLTAuMjY4OS0wLjAzMDMtMC4zOTY2em0wLjExNzYtMS40ODIgMC4wMDY4IDAuNTAwN2gtMC41ODE0Yy0wLjE1MDEgMC0wLjI4MjMgMC4wMTQ2LTAuMzk2NiAwLjA0MzctMC4xMTQyIDAuMDI2OS0wLjIwOTQgMC4wNjcyLTAuMjg1NiAwLjEyMXMtMC4xMzMzIDAuMTE4Ny0wLjE3MTQgMC4xOTQ5LTAuMDU3MSAwLjE2MjQtMC4wNTcxIDAuMjU4OGMwIDAuMDk2MyAwLjAyMjQgMC4xODQ4IDAuMDY3MiAwLjI2NTUgMC4wNDQ4IDAuMDc4NCAwLjEwOTggMC4xNCAwLjE5NDkgMC4xODQ4IDAuMDg3NCAwLjA0NDggMC4xOTI3IDAuMDY3MiAwLjMxNTkgMC4wNjcyIDAuMTY1OCAwIDAuMzEwMy0wLjAzMzYgMC40MzM1LTAuMTAwOCAwLjEyNTUtMC4wNjk1IDAuMjI0LTAuMTUzNSAwLjI5NTctMC4yNTIxIDAuMDcxNy0wLjEwMDggMC4xMDk4LTAuMTk2IDAuMTE0My0wLjI4NTZsMC4yNjIxIDAuMzU5NmMtMC4wMjY5IDAuMDkxOC0wLjA3MjggMC4xOTA0LTAuMTM3OCAwLjI5NTdzLTAuMTUwMSAwLjIwNjEtMC4yNTU0IDAuMzAyNGMtMC4xMDMgMC4wOTQxLTAuMjI3NCAwLjE3MTQtMC4zNzMgMC4yMzE5LTAuMTQzNCAwLjA2MDUtMC4zMDkyIDAuMDkwOC0wLjQ5NzQgMC4wOTA4LTAuMjM3NCAwLTAuNDQ5MS0wLjA0NzEtMC42MzUxLTAuMTQxMi0wLjE4NTktMC4wOTYzLTAuMzMxNi0wLjIyNTEtMC40MzY5LTAuMzg2NC0wLjEwNTMtMC4xNjM2LTAuMTU3OS0wLjM0ODQtMC4xNTc5LTAuNTU0NSAwLTAuMTkyNyAwLjAzNTgtMC4zNjMgMC4xMDc1LTAuNTEwOCAwLjA3NC0wLjE1MDEgMC4xODE1LTAuMjc1NiAwLjMyMjYtMC4zNzY0IDAuMTQzNC0wLjEwMDggMC4zMTgyLTAuMTc3IDAuNTI0My0wLjIyODUgMC4yMDYxLTAuMDUzOCAwLjQ0MTMtMC4wODA3IDAuNzA1Ny0wLjA4MDdoMC42MzUxem00LjAxNDktMS40MjQ4aDAuNzM2djMuNTM1MmMwIDAuMzI3MS0wLjA3IDAuNjA0OS0wLjIwOSAwLjgzMzQtMC4xMzggMC4yMjg2LTAuMzMyIDAuNDAyMi0wLjU4MSAwLjUyMDktMC4yNDkgMC4xMjEtMC41MzYgMC4xODE1LTAuODY0IDAuMTgxNS0wLjEzOCAwLTAuMjkzLTAuMDIwMi0wLjQ2My0wLjA2MDUtMC4xNjgtMC4wNDAzLTAuMzMyLTAuMTA1My0wLjQ5MS0wLjE5NDktMC4xNTctMC4wODc0LTAuMjg4LTAuMjAyOC0wLjM5My0wLjM0NjFsMC4zOC0wLjQ3NzJjMC4xMyAwLjE1NDUgMC4yNzMgMC4yNjc3IDAuNDMgMC4zMzk0czAuMzIxIDAuMTA3NSAwLjQ5NCAwLjEwNzVjMC4xODYgMCAwLjM0NC0wLjAzNDcgMC40NzQtMC4xMDQyIDAuMTMyLTAuMDY3MiAwLjIzNC0wLjE2NjkgMC4zMDUtMC4yOTkgMC4wNzItMC4xMzIyIDAuMTA4LTAuMjkzNSAwLjEwOC0wLjQ4NHYtMi43Mjg3bDAuMDc0LTAuODIzM3ptLTIuNDcgMS44NTgzdi0wLjA3MDVjMC0wLjI3NTYgMC4wMzMtMC41MjY1IDAuMTAxLTAuNzUyOCAwLjA2Ny0wLjIyODUgMC4xNjMtMC40MjQ1IDAuMjg5LTAuNTg4MSAwLjEyNS0wLjE2NTggMC4yNzctMC4yOTI0IDAuNDU3LTAuMzc5NyAwLjE3OS0wLjA4OTYgMC4zODItMC4xMzQ0IDAuNjA4LTAuMTM0NCAwLjIzNSAwIDAuNDM2IDAuMDQyNSAwLjYwMSAwLjEyNzcgMC4xNjkgMC4wODUxIDAuMzA5IDAuMjA3MiAwLjQyMSAwLjM2NjIgMC4xMTIgMC4xNTY5IDAuMTk5IDAuMzQ1MSAwLjI2MiAwLjU2NDYgMC4wNjUgMC4yMTczIDAuMTEzIDAuNDU5MyAwLjE0NCAwLjcyNTl2MC4yMjUxYy0wLjAyOSAwLjI1OTktMC4wNzggMC40OTc0LTAuMTQ4IDAuNzEyNS0wLjA2OSAwLjIxNS0wLjE2MSAwLjQwMS0wLjI3NSAwLjU1NzgtMC4xMTUgMC4xNTY4LTAuMjU2IDAuMjc3OC0wLjQyNCAwLjM2My0wLjE2NSAwLjA4NTEtMC4zNjEgMC4xMjc3LTAuNTg4IDAuMTI3Ny0wLjIyMiAwLTAuNDIyLTAuMDQ2LTAuNjAxLTAuMTM3OC0wLjE3Ny0wLjA5MTktMC4zMy0wLjIyMDctMC40NTctMC4zODY1LTAuMTI2LTAuMTY1OC0wLjIyMi0wLjM2MDctMC4yODktMC41ODQ3LTAuMDY4LTAuMjI2My0wLjEwMS0wLjQ3MTYtMC4xMDEtMC43MzZ6bTAuODEtMC4wNzA1djAuMDcwNWMwIDAuMTY1OCAwLjAxNSAwLjMyMDQgMC4wNDcgMC40NjM4IDAuMDMzIDAuMTQzNCAwLjA4NCAwLjI2OTkgMC4xNTEgMC4zNzk3IDAuMDY5IDAuMTA3NiAwLjE1NyAwLjE5MjcgMC4yNjIgMC4yNTU0IDAuMTA4IDAuMDYwNSAwLjIzNCAwLjA5MDggMC4zOCAwLjA5MDggMC4xOSAwIDAuMzQ2LTAuMDQwNCAwLjQ2Ny0wLjEyMSAwLjEyMy0wLjA4MDcgMC4yMTctMC4xODkzIDAuMjgyLTAuMzI2IDAuMDY3LTAuMTM4OSAwLjExNS0wLjI5MzUgMC4xNDEtMC40NjM3di0wLjYwODNjLTAuMDEzLTAuMTMyMi0wLjA0MS0wLjI1NTQtMC4wODQtMC4zNjk3LTAuMDQtMC4xMTQyLTAuMDk1LTAuMjEzOS0wLjE2NC0wLjI5OS0wLjA3LTAuMDg3NC0wLjE1Ny0wLjE1NDYtMC4yNjItMC4yMDE3LTAuMTA2LTAuMDQ5My0wLjIzLTAuMDczOS0wLjM3My0wLjA3MzktMC4xNDYgMC0wLjI3MyAwLjAzMTQtMC4zOCAwLjA5NDEtMC4xMDggMC4wNjI3LTAuMTk2IDAuMTQ5LTAuMjY2IDAuMjU4Ny0wLjA2NyAwLjEwOTgtMC4xMTcgMC4yMzc1LTAuMTUxIDAuMzgzMS0wLjAzMyAwLjE0NTctMC4wNSAwLjMwMTQtMC4wNSAwLjQ2NzJ6bTMuMjI1IDAuMDcwNXYtMC4wNzczYzAtMC4yNjIxIDAuMDM4LTAuNTA1MiAwLjExNC0wLjcyOTIgMC4wNzYtMC4yMjYzIDAuMTg2LTAuNDIyMyAwLjMyOS0wLjU4ODEgMC4xNDYtMC4xNjggMC4zMjMtMC4yOTggMC41MzEtMC4zODk4IDAuMjExLTAuMDk0MSAwLjQ0OC0wLjE0MTEgMC43MTMtMC4xNDExIDAuMjY2IDAgMC41MDQgMC4wNDcgMC43MTIgMC4xNDExIDAuMjExIDAuMDkxOCAwLjM4OSAwLjIyMTggMC41MzQgMC4zODk4IDAuMTQ2IDAuMTY1OCAwLjI1NyAwLjM2MTggMC4zMzMgMC41ODgxIDAuMDc2IDAuMjI0IDAuMTE0IDAuNDY3MSAwLjExNCAwLjcyOTJ2MC4wNzczYzAgMC4yNjIyLTAuMDM4IDAuNTA1Mi0wLjExNCAwLjcyOTMtMC4wNzYgMC4yMjQtMC4xODcgMC40Mi0wLjMzMyAwLjU4ODEtMC4xNDUgMC4xNjU3LTAuMzIyIDAuMjk1Ny0wLjUzMSAwLjM4OTgtMC4yMDggMC4wOTE4LTAuNDQ0IDAuMTM3OC0wLjcwOSAwLjEzNzgtMC4yNjYgMC0wLjUwNS0wLjA0Ni0wLjcxNS0wLjEzNzgtMC4yMDktMC4wOTQxLTAuMzg2LTAuMjI0MS0wLjUzMS0wLjM4OTgtMC4xNDYtMC4xNjgxLTAuMjU3LTAuMzY0MS0wLjMzMy0wLjU4ODEtMC4wNzYtMC4yMjQxLTAuMTE0LTAuNDY3MS0wLjExNC0wLjcyOTN6bTAuODEtMC4wNzczdjAuMDc3M2MwIDAuMTYzNiAwLjAxNiAwLjMxODIgMC4wNSAwLjQ2MzhzMC4wODYgMC4yNzMzIDAuMTU4IDAuMzgzMSAwLjE2NCAwLjE5NiAwLjI3NiAwLjI1ODdjMC4xMTIgMC4wNjI4IDAuMjQ1IDAuMDk0MSAwLjM5OSAwLjA5NDEgMC4xNTEgMCAwLjI4LTAuMDMxMyAwLjM5LTAuMDk0MSAwLjExMi0wLjA2MjcgMC4yMDQtMC4xNDg5IDAuMjc2LTAuMjU4N3MwLjEyNC0wLjIzNzUgMC4xNTgtMC4zODMxYzAuMDM2LTAuMTQ1NiAwLjA1NC0wLjMwMDIgMC4wNTQtMC40NjM4di0wLjA3NzNjMC0wLjE2MTMtMC4wMTgtMC4zMTM2LTAuMDU0LTAuNDU3LTAuMDM0LTAuMTQ1Ni0wLjA4OC0wLjI3NDQtMC4xNjItMC4zODY1LTAuMDcxLTAuMTEyLTAuMTYzLTAuMTk5My0wLjI3NS0wLjI2MjEtMC4xMS0wLjA2NDktMC4yNDEtMC4wOTc0LTAuMzkzLTAuMDk3NC0wLjE1MyAwLTAuMjg1IDAuMDMyNS0wLjM5NyAwLjA5NzQtMC4xMSAwLjA2MjgtMC4yIDAuMTUwMS0wLjI3MiAwLjI2MjEtMC4wNzIgMC4xMTIxLTAuMTI0IDAuMjQwOS0wLjE1OCAwLjM4NjUtMC4wMzQgMC4xNDM0LTAuMDUgMC4yOTU3LTAuMDUgMC40NTd6IiBmaWxsLW9wYWNpdHk9Ii4zOCIvPgogICA8cGF0aCBkPSJtNDguMTk2IDgwLjQ2OXYyLjc5NTloLTE0LjIxM3YtMi40MDI3bDYuOTAyNS03LjUyODdjMC43NTcyLTAuODU0MyAxLjM1NDMtMS41OTIyIDEuNzkxMS0yLjIxMzUgMC40MzY5LTAuNjIxMyAwLjc0MjctMS4xNzk1IDAuOTE3NS0xLjY3NDYgMC4xODQ0LTAuNTA0OSAwLjI3NjYtMC45OTUxIDAuMjc2Ni0xLjQ3MDggMC0wLjY2OTktMC4xMjYyLTEuMjU3Mi0wLjM3ODYtMS43NjIxLTAuMjQyNy0wLjUxNDUtMC42MDE5LTAuOTE3NC0xLjA3NzYtMS4yMDg2LTAuNDc1Ny0wLjMwMS0xLjA1MzMtMC40NTE1LTEuNzMyOS0wLjQ1MTUtMC43ODY0IDAtMS40NDY1IDAuMTY5OS0xLjk4MDUgMC41MDk3LTAuNTMzOSAwLjMzOTgtMC45MzY4IDAuODEwNi0xLjIwODYgMS40MTI2LTAuMjcxOSAwLjU5MjEtMC40MDc4IDEuMjcxNy0wLjQwNzggMi4wMzg3aC0zLjUwOTVjMC0xLjIzMyAwLjI4MTYtMi4zNTkxIDAuODQ0Ni0zLjM3ODUgMC41NjMxLTEuMDI5IDEuMzc4Ni0xLjg0NDUgMi40NDY1LTIuNDQ2NCAxLjA2NzktMC42MTE3IDIuMzU0Mi0wLjkxNzUgMy44NTktMC45MTc1IDEuNDE3NCAwIDIuNjIxMiAwLjIzNzkgMy42MTE0IDAuNzEzNiAwLjk5MDMgMC40NzU3IDEuNzQyNyAxLjE1MDQgMi4yNTcyIDIuMDI0MSAwLjUyNDIgMC44NzM4IDAuNzg2NCAxLjkwNzcgMC43ODY0IDMuMTAxOCAwIDAuNjYwMi0wLjEwNjggMS4zMTU1LTAuMzIwNCAxLjk2NTktMC4yMTM2IDAuNjUwNS0wLjUxOTQgMS4zMDA5LTAuOTE3NCAxLjk1MTQtMC4zODg0IDAuNjQwNy0wLjg0OTUgMS4yODYzLTEuMzgzNSAxLjkzNjctMC41MzM5IDAuNjQwOC0xLjEyMTIgMS4yOTEyLTEuNzYyIDEuOTUxNGwtNC41ODcxIDUuMDUzMWg5Ljc4NTh6bTE2LjQyOSAwdjIuNzk1OWgtMTQuMjEzdi0yLjQwMjdsNi45MDI2LTcuNTI4N2MwLjc1NzItMC44NTQzIDEuMzU0Mi0xLjU5MjIgMS43OTExLTIuMjEzNXMwLjc0MjctMS4xNzk1IDAuOTE3NC0xLjY3NDZjMC4xODQ1LTAuNTA0OSAwLjI3NjctMC45OTUxIDAuMjc2Ny0xLjQ3MDggMC0wLjY2OTktMC4xMjYyLTEuMjU3Mi0wLjM3ODYtMS43NjIxLTAuMjQyNy0wLjUxNDUtMC42MDE5LTAuOTE3NC0xLjA3NzYtMS4yMDg2LTAuNDc1Ny0wLjMwMS0xLjA1MzMtMC40NTE1LTEuNzMyOS0wLjQ1MTUtMC43ODY0IDAtMS40NDY1IDAuMTY5OS0xLjk4MDUgMC41MDk3LTAuNTMzOSAwLjMzOTgtMC45MzY4IDAuODEwNi0xLjIwODcgMS40MTI2LTAuMjcxOCAwLjU5MjEtMC40MDc3IDEuMjcxNy0wLjQwNzcgMi4wMzg3aC0zLjUwOTVjMC0xLjIzMyAwLjI4MTUtMi4zNTkxIDAuODQ0Ni0zLjM3ODUgMC41NjMxLTEuMDI5IDEuMzc4Ni0xLjg0NDUgMi40NDY1LTIuNDQ2NCAxLjA2NzktMC42MTE3IDIuMzU0Mi0wLjkxNzUgMy44NTktMC45MTc1IDEuNDE3NCAwIDIuNjIxMiAwLjIzNzkgMy42MTE0IDAuNzEzNnMxLjc0MjYgMS4xNTA0IDIuMjU3MiAyLjAyNDFjMC41MjQyIDAuODczOCAwLjc4NjMgMS45MDc3IDAuNzg2MyAzLjEwMTggMCAwLjY2MDItMC4xMDY4IDEuMzE1NS0wLjMyMDMgMS45NjU5LTAuMjEzNiAwLjY1MDUtMC41MTk0IDEuMzAwOS0wLjkxNzUgMS45NTE0LTAuMzg4MyAwLjY0MDctMC44NDk0IDEuMjg2My0xLjM4MzQgMS45MzY3LTAuNTMzOSAwLjY0MDgtMS4xMjEzIDEuMjkxMi0xLjc2MiAxLjk1MTRsLTQuNTg3MSA1LjA1MzFoOS43ODU4em0yLjQ5MjUtMTQuODFjMC0wLjcwODcgMC4xNzQ3LTEuMzU5MiAwLjUyNDItMS45NTE0czAuODE1NS0xLjA2MyAxLjM5OC0xLjQxMjVjMC41OTIyLTAuMzU5MiAxLjIzMjktMC41Mzg4IDEuOTIyMi0wLjUzODggMC42OTkgMCAxLjMzNDkgMC4xNzk2IDEuOTA3NyAwLjUzODggMC41NzI4IDAuMzQ5NSAxLjAyOTEgMC44MjAzIDEuMzY4OCAxLjQxMjUgMC4zNDk1IDAuNTkyMiAwLjUyNDMgMS4yNDI3IDAuNTI0MyAxLjk1MTRzLTAuMTc0OCAxLjM1OTEtMC41MjQzIDEuOTUxM2MtMC4zMzk3IDAuNTgyNS0wLjc5NiAxLjA0MzYtMS4zNjg4IDEuMzgzNHMtMS4yMDg3IDAuNTA5Ny0xLjkwNzcgMC41MDk3Yy0wLjY4OTMgMC0xLjMzLTAuMTY5OS0xLjkyMjItMC41MDk3LTAuNTgyNS0wLjMzOTgtMS4wNDg1LTAuODAwOS0xLjM5OC0xLjM4MzQtMC4zNDk1LTAuNTkyMi0wLjUyNDItMS4yNDI2LTAuNTI0Mi0xLjk1MTN6bTEuOTY1OSAwYzAgMC41MjQyIDAuMTg0NSAwLjk2NTkgMC41NTM0IDEuMzI1MSAwLjM2ODkgMC4zNDk1IDAuODEwNiAwLjUyNDMgMS4zMjUxIDAuNTI0MyAwLjUxNDYgMCAwLjk0NjYtMC4xNzQ4IDEuMjk2MS0wLjUyNDNzMC41MjQyLTAuNzkxMiAwLjUyNDItMS4zMjUxYzAtMC41NDM3LTAuMTc0Ny0wLjk5NTEtMC41MjQyLTEuMzU0M3MtMC43ODE1LTAuNTM4OC0xLjI5NjEtMC41Mzg4Yy0wLjUxNDUgMC0wLjk1NjIgMC4xNzk2LTEuMzI1MSAwLjUzODhzLTAuNTUzNCAwLjgxMDYtMC41NTM0IDEuMzU0M3ptMjEuNzI5IDEwLjcwM2gzLjY0MDZjLTAuMTE2NSAxLjM4ODMtMC41MDQ4IDIuNjI2MS0xLjE2NSAzLjcxMzQtMC42NjAxIDEuMDc3Ni0xLjU4NzMgMS45MjcxLTIuNzgxNCAyLjU0ODRzLTIuNjQ1NCAwLjkzMi00LjM1NDEgMC45MzJjLTEuMzEwNiAwLTIuNDkwMS0wLjIzMy0zLjUzODYtMC42OTktMS4wNDg1LTAuNDc1Ny0xLjk0NjUtMS4xNDU2LTIuNjk0LTIuMDA5Ni0wLjc0NzYtMC44NzM3LTEuMzIwNC0xLjkyNzEtMS43MTg0LTMuMTYtMC4zODgzLTEuMjMyOS0wLjU4MjUtMi42MTE1LTAuNTgyNS00LjEzNTd2LTEuNzYyYzAtMS41MjQyIDAuMTk5LTIuOTAyOCAwLjU5NzEtNC4xMzU3IDAuNDA3Ny0xLjIzMjkgMC45OTAyLTIuMjg2MyAxLjc0NzQtMy4xNiAwLjc1NzMtMC44ODM1IDEuNjY1LTEuNTU4MiAyLjcyMzItMi4wMjQyIDEuMDY3OS0wLjQ2NiAyLjI2NjktMC42OTkgMy41OTY5LTAuNjk5IDEuNjg5MiAwIDMuMTE2MyAwLjMxMDcgNC4yODEzIDAuOTMyczIuMDY3OCAxLjQ4MDUgMi43MDg2IDIuNTc3NWMwLjY1MDQgMS4wOTcxIDEuMDQ4NCAyLjM1NDMgMS4xOTQxIDMuNzcxN2gtMy42NDA2Yy0wLjA5NzEtMC45MTI2LTAuMzEwNy0xLjY5NDEtMC42NDA3LTIuMzQ0Ni0wLjMyMDQtMC42NTA0LTAuNzk2MS0xLjE0NTUtMS40MjcxLTEuNDg1My0wLjYzMTEtMC4zNDk1LTEuNDU2My0wLjUyNDItMi40NzU2LTAuNTI0Mi0wLjgzNDkgMC0xLjU2MyAwLjE1NTMtMi4xODQ0IDAuNDY1OS0wLjYyMTMgMC4zMTA3LTEuMTQwNyAwLjc2Ny0xLjU1ODEgMS4zNjg5LTAuNDE3NSAwLjYwMTktMC43MzMgMS4zNDQ2LTAuOTQ2NiAyLjIyOC0wLjIwMzkgMC44NzM4LTAuMzA1OCAxLjg3MzctMC4zMDU4IDIuOTk5OXYxLjc5MTFjMCAxLjA2NzkgMC4wOTIyIDIuMDM4NyAwLjI3NjcgMi45MTI1IDAuMTk0MiAwLjg2NCAwLjQ4NTQgMS42MDY3IDAuODczNyAyLjIyOCAwLjM5ODEgMC42MjEzIDAuOTAyOSAxLjEwMTkgMS41MTQ1IDEuNDQxNyAwLjYxMTYgMC4zMzk3IDEuMzQ0NiAwLjUwOTYgMi4xOTg5IDAuNTA5NiAxLjAzODggMCAxLjg3ODUtMC4xNjUgMi41MTkzLTAuNDk1MSAwLjY1MDQtMC4zMzAxIDEuMTQwNy0wLjgxMDYgMS40NzA4LTEuNDQxNiAwLjMzOTgtMC42NDA4IDAuNTYzLTEuNDIyMyAwLjY2OTgtMi4zNDQ2eiIgZmlsbC1vcGFjaXR5PSIuODciLz4KICA8L2c+CiA8L2c+CiA8ZGVmcz4KICA8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfMTE0Ml8yMDM5NTQiIHg9Ii45MTE3NiIgeT0iLjIwNTg4IiB3aWR0aD0iMTI2LjE4IiBoZWlnaHQ9IjEyNi4xOCIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgogICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0iaGFyZEFscGhhIiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIi8+CiAgIDxmZU9mZnNldCBkeT0iMi4yOTQxMiIvPgogICA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIyLjI5NDEyIi8+CiAgIDxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgogICA8ZmVDb2xvck1hdHJpeCB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDQgMCIvPgogICA8ZmVCbGVuZCBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfMTE0Ml8yMDM5NTQiLz4KICAgPGZlQmxlbmQgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzExNDJfMjAzOTU0IiByZXN1bHQ9InNoYXBlIi8+CiAgPC9maWx0ZXI+CiA8L2RlZnM+Cjwvc3ZnPgo=", "description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.", "descriptor": { "type": "latest", - "sizeX": 2.5, - "sizeY": 2.5, + "sizeX": 3, + "sizeY": 3, "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px'\n };\n};\n\nself.onDestroy = function() {\n};\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px',\n absoluteHeader: true\n };\n};\n\nself.onDestroy = function() {\n};\n", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "", + "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" @@ -250,7 +250,7 @@ { "alias": "horizontal_value_card", "name": "Horizontal value card", - "image": null, + "image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzk5IiBoZWlnaHQ9IjEwOCIgdmlld0JveD0iMCAwIDM5OSAxMDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfMTI0Nl80NDQ0NykiPgo8cmVjdCB4PSI4IiB5PSI0IiB3aWR0aD0iMzgzIiBoZWlnaHQ9IjkyIiByeD0iNCIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTU3LjAwMDEgNTEuNjY2N1YzOC4zMzM0QzU3LjAwMDEgMzUuNTY2NyA1NC43NjY3IDMzLjMzMzQgNTIuMDAwMSAzMy4zMzM0QzQ5LjIzMzQgMzMuMzMzNCA0Ny4wMDAxIDM1LjU2NjcgNDcuMDAwMSAzOC4zMzM0VjUxLjY2NjdDNDQuOTgzNCA1My4xODM0IDQzLjY2NjcgNTUuNjE2NyA0My42NjY3IDU4LjMzMzRDNDMuNjY2NyA2Mi45MzM0IDQ3LjQwMDEgNjYuNjY2NyA1Mi4wMDAxIDY2LjY2NjdDNTYuNjAwMSA2Ni42NjY3IDYwLjMzMzQgNjIuOTMzNCA2MC4zMzM0IDU4LjMzMzRDNjAuMzMzNCA1NS42MTY3IDU5LjAxNjcgNTMuMTgzNCA1Ny4wMDAxIDUxLjY2NjdaTTUwLjMzMzQgMzguMzMzNEM1MC4zMzM0IDM3LjQxNjcgNTEuMDgzNCAzNi42NjY3IDUyLjAwMDEgMzYuNjY2N0M1Mi45MTY3IDM2LjY2NjcgNTMuNjY2NyAzNy40MTY3IDUzLjY2NjcgMzguMzMzNEg1Mi4wMDAxVjQwSDUzLjY2NjdWNDMuMzMzNEg1Mi4wMDAxVjQ1SDUzLjY2NjdWNDguMzMzNEg1MC4zMzM0VjM4LjMzMzRaIiBmaWxsPSIjNTQ2OUZGIi8+CjxwYXRoIGQ9Ik04NS44MzU5IDM1LjYyNVY0N0g4My44OTA2VjM1LjYyNUg4NS44MzU5Wk04OS40MDYyIDM1LjYyNVYzNy4xODc1SDgwLjM1MTZWMzUuNjI1SDg5LjQwNjJaTTkzLjk0NTMgNDcuMTU2MkM5My4zMjAzIDQ3LjE1NjIgOTIuNzU1MiA0Ny4wNTQ3IDkyLjI1IDQ2Ljg1MTZDOTEuNzUgNDYuNjQzMiA5MS4zMjI5IDQ2LjM1NDIgOTAuOTY4OCA0NS45ODQ0QzkwLjYxOTggNDUuNjE0NiA5MC4zNTE2IDQ1LjE3OTcgOTAuMTY0MSA0NC42Nzk3Qzg5Ljk3NjYgNDQuMTc5NyA4OS44ODI4IDQzLjY0MDYgODkuODgyOCA0My4wNjI1VjQyLjc1Qzg5Ljg4MjggNDIuMDg4NSA4OS45NzkyIDQxLjQ4OTYgOTAuMTcxOSA0MC45NTMxQzkwLjM2NDYgNDAuNDE2NyA5MC42MzI4IDM5Ljk1ODMgOTAuOTc2NiAzOS41NzgxQzkxLjMyMDMgMzkuMTkyNyA5MS43MjY2IDM4Ljg5ODQgOTIuMTk1MyAzOC42OTUzQzkyLjY2NDEgMzguNDkyMiA5My4xNzE5IDM4LjM5MDYgOTMuNzE4OCAzOC4zOTA2Qzk0LjMyMjkgMzguMzkwNiA5NC44NTE2IDM4LjQ5MjIgOTUuMzA0NyAzOC42OTUzQzk1Ljc1NzggMzguODk4NCA5Ni4xMzI4IDM5LjE4NDkgOTYuNDI5NyAzOS41NTQ3Qzk2LjczMTggMzkuOTE5MyA5Ni45NTU3IDQwLjM1NDIgOTcuMTAxNiA0MC44NTk0Qzk3LjI1MjYgNDEuMzY0NiA5Ny4zMjgxIDQxLjkyMTkgOTcuMzI4MSA0Mi41MzEyVjQzLjMzNTlIOTAuNzk2OVY0MS45ODQ0SDk1LjQ2ODhWNDEuODM1OUM5NS40NTgzIDQxLjQ5NzQgOTUuMzkwNiA0MS4xNzk3IDk1LjI2NTYgNDAuODgyOEM5NS4xNDU4IDQwLjU4NTkgOTQuOTYwOSA0MC4zNDY0IDk0LjcxMDkgNDAuMTY0MUM5NC40NjA5IDM5Ljk4MTggOTQuMTI3NiAzOS44OTA2IDkzLjcxMDkgMzkuODkwNkM5My4zOTg0IDM5Ljg5MDYgOTMuMTE5OCAzOS45NTgzIDkyLjg3NSA0MC4wOTM4QzkyLjYzNTQgNDAuMjI0IDkyLjQzNDkgNDAuNDE0MSA5Mi4yNzM0IDQwLjY2NDFDOTIuMTEyIDQwLjkxNDEgOTEuOTg3IDQxLjIxNjEgOTEuODk4NCA0MS41NzAzQzkxLjgxNTEgNDEuOTE5MyA5MS43NzM0IDQyLjMxMjUgOTEuNzczNCA0Mi43NVY0My4wNjI1QzkxLjc3MzQgNDMuNDMyMyA5MS44MjI5IDQzLjc3NiA5MS45MjE5IDQ0LjA5MzhDOTIuMDI2IDQ0LjQwNjIgOTIuMTc3MSA0NC42Nzk3IDkyLjM3NSA0NC45MTQxQzkyLjU3MjkgNDUuMTQ4NCA5Mi44MTI1IDQ1LjMzMzMgOTMuMDkzOCA0NS40Njg4QzkzLjM3NSA0NS41OTkgOTMuNjk1MyA0NS42NjQxIDk0LjA1NDcgNDUuNjY0MUM5NC41MDc4IDQ1LjY2NDEgOTQuOTExNSA0NS41NzI5IDk1LjI2NTYgNDUuMzkwNkM5NS42MTk4IDQ1LjIwODMgOTUuOTI3MSA0NC45NTA1IDk2LjE4NzUgNDQuNjE3Mkw5Ny4xNzk3IDQ1LjU3ODFDOTYuOTk3NCA0NS44NDM4IDk2Ljc2MDQgNDYuMDk5IDk2LjQ2ODggNDYuMzQzOEM5Ni4xNzcxIDQ2LjU4MzMgOTUuODIwMyA0Ni43Nzg2IDk1LjM5ODQgNDYuOTI5N0M5NC45ODE4IDQ3LjA4MDcgOTQuNDk3NCA0Ny4xNTYyIDkzLjk0NTMgNDcuMTU2MlpNMTAwLjkzIDQwLjI2NTZWNDdIOTkuMDQ2OVYzOC41NDY5SDEwMC44MkwxMDAuOTMgNDAuMjY1NlpNMTAwLjYyNSA0Mi40NjA5TDk5Ljk4NDQgNDIuNDUzMUM5OS45ODQ0IDQxLjg2OTggMTAwLjA1NyA0MS4zMzA3IDEwMC4yMDMgNDAuODM1OUMxMDAuMzQ5IDQwLjM0MTEgMTAwLjU2MiAzOS45MTE1IDEwMC44NDQgMzkuNTQ2OUMxMDEuMTI1IDM5LjE3NzEgMTAxLjQ3NCAzOC44OTMyIDEwMS44OTEgMzguNjk1M0MxMDIuMzEyIDM4LjQ5MjIgMTAyLjc5OSAzOC4zOTA2IDEwMy4zNTIgMzguMzkwNkMxMDMuNzM3IDM4LjM5MDYgMTA0LjA4OSAzOC40NDc5IDEwNC40MDYgMzguNTYyNUMxMDQuNzI5IDM4LjY3MTkgMTA1LjAwOCAzOC44NDY0IDEwNS4yNDIgMzkuMDg1OUMxMDUuNDgyIDM5LjMyNTUgMTA1LjY2NCAzOS42MzI4IDEwNS43ODkgNDAuMDA3OEMxMDUuOTE5IDQwLjM4MjggMTA1Ljk4NCA0MC44MzU5IDEwNS45ODQgNDEuMzY3MlY0N0gxMDQuMTAyVjQxLjUzMTJDMTA0LjEwMiA0MS4xMTk4IDEwNC4wMzkgNDAuNzk2OSAxMDMuOTE0IDQwLjU2MjVDMTAzLjc5NCA0MC4zMjgxIDEwMy42MiA0MC4xNjE1IDEwMy4zOTEgNDAuMDYyNUMxMDMuMTY3IDM5Ljk1ODMgMTAyLjg5OCAzOS45MDYyIDEwMi41ODYgMzkuOTA2MkMxMDIuMjMyIDM5LjkwNjIgMTAxLjkzIDM5Ljk3NCAxMDEuNjggNDAuMTA5NEMxMDEuNDM1IDQwLjI0NDggMTAxLjIzNCA0MC40Mjk3IDEwMS4wNzggNDAuNjY0MUMxMDAuOTIyIDQwLjg5ODQgMTAwLjgwNyA0MS4xNjkzIDEwMC43MzQgNDEuNDc2NkMxMDAuNjYxIDQxLjc4MzkgMTAwLjYyNSA0Mi4xMTIgMTAwLjYyNSA0Mi40NjA5Wk0xMDUuODY3IDQxLjk2MDlMMTA0Ljk4NCA0Mi4xNTYyQzEwNC45ODQgNDEuNjQ1OCAxMDUuMDU1IDQxLjE2NDEgMTA1LjE5NSA0MC43MTA5QzEwNS4zNDEgNDAuMjUyNiAxMDUuNTUyIDM5Ljg1MTYgMTA1LjgyOCAzOS41MDc4QzEwNi4xMDkgMzkuMTU4OSAxMDYuNDU2IDM4Ljg4NTQgMTA2Ljg2NyAzOC42ODc1QzEwNy4yNzkgMzguNDg5NiAxMDcuNzUgMzguMzkwNiAxMDguMjgxIDM4LjM5MDZDMTA4LjcxNCAzOC4zOTA2IDEwOS4wOTkgMzguNDUwNSAxMDkuNDM4IDM4LjU3MDNDMTA5Ljc4MSAzOC42ODQ5IDExMC4wNzMgMzguODY3MiAxMTAuMzEyIDM5LjExNzJDMTEwLjU1MiAzOS4zNjcyIDExMC43MzQgMzkuNjkyNyAxMTAuODU5IDQwLjA5MzhDMTEwLjk4NCA0MC40ODk2IDExMS4wNDcgNDAuOTY4OCAxMTEuMDQ3IDQxLjUzMTJWNDdIMTA5LjE1NlY0MS41MjM0QzEwOS4xNTYgNDEuMDk2NCAxMDkuMDk0IDQwLjc2NTYgMTA4Ljk2OSA0MC41MzEyQzEwOC44NDkgNDAuMjk2OSAxMDguNjc3IDQwLjEzNTQgMTA4LjQ1MyA0MC4wNDY5QzEwOC4yMjkgMzkuOTUzMSAxMDcuOTYxIDM5LjkwNjIgMTA3LjY0OCAzOS45MDYyQzEwNy4zNTcgMzkuOTA2MiAxMDcuMDk5IDM5Ljk2MDkgMTA2Ljg3NSA0MC4wNzAzQzEwNi42NTYgNDAuMTc0NSAxMDYuNDcxIDQwLjMyMjkgMTA2LjMyIDQwLjUxNTZDMTA2LjE2OSA0MC43MDMxIDEwNi4wNTUgNDAuOTE5MyAxMDUuOTc3IDQxLjE2NDFDMTA1LjkwNCA0MS40MDg5IDEwNS44NjcgNDEuNjc0NSAxMDUuODY3IDQxLjk2MDlaTTExNS4xMjUgNDAuMTcxOVY1MC4yNUgxMTMuMjQyVjM4LjU0NjlIMTE0Ljk3N0wxMTUuMTI1IDQwLjE3MTlaTTEyMC42MzMgNDIuNjk1M1Y0Mi44NTk0QzEyMC42MzMgNDMuNDc0IDEyMC41NiA0NC4wNDQzIDEyMC40MTQgNDQuNTcwM0MxMjAuMjczIDQ1LjA5MTEgMTIwLjA2MiA0NS41NDY5IDExOS43ODEgNDUuOTM3NUMxMTkuNTA1IDQ2LjMyMjkgMTE5LjE2NCA0Ni42MjI0IDExOC43NTggNDYuODM1OUMxMTguMzUyIDQ3LjA0OTUgMTE3Ljg4MyA0Ny4xNTYyIDExNy4zNTIgNDcuMTU2MkMxMTYuODI2IDQ3LjE1NjIgMTE2LjM2NSA0Ny4wNTk5IDExNS45NjkgNDYuODY3MkMxMTUuNTc4IDQ2LjY2OTMgMTE1LjI0NyA0Ni4zOTA2IDExNC45NzcgNDYuMDMxMkMxMTQuNzA2IDQ1LjY3MTkgMTE0LjQ4NyA0NS4yNSAxMTQuMzIgNDQuNzY1NkMxMTQuMTU5IDQ0LjI3NiAxMTQuMDQ0IDQzLjczOTYgMTEzLjk3NyA0My4xNTYyVjQyLjUyMzRDMTE0LjA0NCA0MS45MDM2IDExNC4xNTkgNDEuMzQxMSAxMTQuMzIgNDAuODM1OUMxMTQuNDg3IDQwLjMzMDcgMTE0LjcwNiAzOS44OTU4IDExNC45NzcgMzkuNTMxMkMxMTUuMjQ3IDM5LjE2NjcgMTE1LjU3OCAzOC44ODU0IDExNS45NjkgMzguNjg3NUMxMTYuMzU5IDM4LjQ4OTYgMTE2LjgxNSAzOC4zOTA2IDExNy4zMzYgMzguMzkwNkMxMTcuODY3IDM4LjM5MDYgMTE4LjMzOSAzOC40OTQ4IDExOC43NSAzOC43MDMxQzExOS4xNjEgMzguOTA2MiAxMTkuNTA4IDM5LjE5NzkgMTE5Ljc4OSAzOS41NzgxQzEyMC4wNyAzOS45NTMxIDEyMC4yODEgNDAuNDA2MiAxMjAuNDIyIDQwLjkzNzVDMTIwLjU2MiA0MS40NjM1IDEyMC42MzMgNDIuMDQ5NSAxMjAuNjMzIDQyLjY5NTNaTTExOC43NSA0Mi44NTk0VjQyLjY5NTNDMTE4Ljc1IDQyLjMwNDcgMTE4LjcxNCA0MS45NDI3IDExOC42NDEgNDEuNjA5NEMxMTguNTY4IDQxLjI3MDggMTE4LjQ1MyA0MC45NzQgMTE4LjI5NyA0MC43MTg4QzExOC4xNDEgNDAuNDYzNSAxMTcuOTQgNDAuMjY1NiAxMTcuNjk1IDQwLjEyNUMxMTcuNDU2IDM5Ljk3OTIgMTE3LjE2NyAzOS45MDYyIDExNi44MjggMzkuOTA2MkMxMTYuNDk1IDM5LjkwNjIgMTE2LjIwOCAzOS45NjM1IDExNS45NjkgNDAuMDc4MUMxMTUuNzI5IDQwLjE4NzUgMTE1LjUyOSA0MC4zNDExIDExNS4zNjcgNDAuNTM5MUMxMTUuMjA2IDQwLjczNyAxMTUuMDgxIDQwLjk2ODggMTE0Ljk5MiA0MS4yMzQ0QzExNC45MDQgNDEuNDk0OCAxMTQuODQxIDQxLjc3ODYgMTE0LjgwNSA0Mi4wODU5VjQzLjYwMTZDMTE0Ljg2NyA0My45NzY2IDExNC45NzQgNDQuMzIwMyAxMTUuMTI1IDQ0LjYzMjhDMTE1LjI3NiA0NC45NDUzIDExNS40OSA0NS4xOTUzIDExNS43NjYgNDUuMzgyOEMxMTYuMDQ3IDQ1LjU2NTEgMTE2LjQwNiA0NS42NTYyIDExNi44NDQgNDUuNjU2MkMxMTcuMTgyIDQ1LjY1NjIgMTE3LjQ3MSA0NS41ODMzIDExNy43MTEgNDUuNDM3NUMxMTcuOTUxIDQ1LjI5MTcgMTE4LjE0NiA0NS4wOTExIDExOC4yOTcgNDQuODM1OUMxMTguNDUzIDQ0LjU3NTUgMTE4LjU2OCA0NC4yNzYgMTE4LjY0MSA0My45Mzc1QzExOC43MTQgNDMuNTk5IDExOC43NSA0My4yMzk2IDExOC43NSA0Mi44NTk0Wk0xMjYuMjExIDQ3LjE1NjJDMTI1LjU4NiA0Ny4xNTYyIDEyNS4wMjEgNDcuMDU0NyAxMjQuNTE2IDQ2Ljg1MTZDMTI0LjAxNiA0Ni42NDMyIDEyMy41ODkgNDYuMzU0MiAxMjMuMjM0IDQ1Ljk4NDRDMTIyLjg4NSA0NS42MTQ2IDEyMi42MTcgNDUuMTc5NyAxMjIuNDMgNDQuNjc5N0MxMjIuMjQyIDQ0LjE3OTcgMTIyLjE0OCA0My42NDA2IDEyMi4xNDggNDMuMDYyNVY0Mi43NUMxMjIuMTQ4IDQyLjA4ODUgMTIyLjI0NSA0MS40ODk2IDEyMi40MzggNDAuOTUzMUMxMjIuNjMgNDAuNDE2NyAxMjIuODk4IDM5Ljk1ODMgMTIzLjI0MiAzOS41NzgxQzEyMy41ODYgMzkuMTkyNyAxMjMuOTkyIDM4Ljg5ODQgMTI0LjQ2MSAzOC42OTUzQzEyNC45MyAzOC40OTIyIDEyNS40MzggMzguMzkwNiAxMjUuOTg0IDM4LjM5MDZDMTI2LjU4OSAzOC4zOTA2IDEyNy4xMTcgMzguNDkyMiAxMjcuNTcgMzguNjk1M0MxMjguMDIzIDM4Ljg5ODQgMTI4LjM5OCAzOS4xODQ5IDEyOC42OTUgMzkuNTU0N0MxMjguOTk3IDM5LjkxOTMgMTI5LjIyMSA0MC4zNTQyIDEyOS4zNjcgNDAuODU5NEMxMjkuNTE4IDQxLjM2NDYgMTI5LjU5NCA0MS45MjE5IDEyOS41OTQgNDIuNTMxMlY0My4zMzU5SDEyMy4wNjJWNDEuOTg0NEgxMjcuNzM0VjQxLjgzNTlDMTI3LjcyNCA0MS40OTc0IDEyNy42NTYgNDEuMTc5NyAxMjcuNTMxIDQwLjg4MjhDMTI3LjQxMSA0MC41ODU5IDEyNy4yMjcgNDAuMzQ2NCAxMjYuOTc3IDQwLjE2NDFDMTI2LjcyNyAzOS45ODE4IDEyNi4zOTMgMzkuODkwNiAxMjUuOTc3IDM5Ljg5MDZDMTI1LjY2NCAzOS44OTA2IDEyNS4zODUgMzkuOTU4MyAxMjUuMTQxIDQwLjA5MzhDMTI0LjkwMSA0MC4yMjQgMTI0LjcwMSA0MC40MTQxIDEyNC41MzkgNDAuNjY0MUMxMjQuMzc4IDQwLjkxNDEgMTI0LjI1MyA0MS4yMTYxIDEyNC4xNjQgNDEuNTcwM0MxMjQuMDgxIDQxLjkxOTMgMTI0LjAzOSA0Mi4zMTI1IDEyNC4wMzkgNDIuNzVWNDMuMDYyNUMxMjQuMDM5IDQzLjQzMjMgMTI0LjA4OSA0My43NzYgMTI0LjE4OCA0NC4wOTM4QzEyNC4yOTIgNDQuNDA2MiAxMjQuNDQzIDQ0LjY3OTcgMTI0LjY0MSA0NC45MTQxQzEyNC44MzkgNDUuMTQ4NCAxMjUuMDc4IDQ1LjMzMzMgMTI1LjM1OSA0NS40Njg4QzEyNS42NDEgNDUuNTk5IDEyNS45NjEgNDUuNjY0MSAxMjYuMzIgNDUuNjY0MUMxMjYuNzczIDQ1LjY2NDEgMTI3LjE3NyA0NS41NzI5IDEyNy41MzEgNDUuMzkwNkMxMjcuODg1IDQ1LjIwODMgMTI4LjE5MyA0NC45NTA1IDEyOC40NTMgNDQuNjE3MkwxMjkuNDQ1IDQ1LjU3ODFDMTI5LjI2MyA0NS44NDM4IDEyOS4wMjYgNDYuMDk5IDEyOC43MzQgNDYuMzQzOEMxMjguNDQzIDQ2LjU4MzMgMTI4LjA4NiA0Ni43Nzg2IDEyNy42NjQgNDYuOTI5N0MxMjcuMjQ3IDQ3LjA4MDcgMTI2Ljc2MyA0Ny4xNTYyIDEyNi4yMTEgNDcuMTU2MlpNMTMzLjIwMyA0MC4xNTYyVjQ3SDEzMS4zMlYzOC41NDY5SDEzMy4xMTdMMTMzLjIwMyA0MC4xNTYyWk0xMzUuNzg5IDM4LjQ5MjJMMTM1Ljc3MyA0MC4yNDIyQzEzNS42NTkgNDAuMjIxNCAxMzUuNTM0IDQwLjIwNTcgMTM1LjM5OCA0MC4xOTUzQzEzNS4yNjggNDAuMTg0OSAxMzUuMTM4IDQwLjE3OTcgMTM1LjAwOCA0MC4xNzk3QzEzNC42ODUgNDAuMTc5NyAxMzQuNDAxIDQwLjIyNjYgMTM0LjE1NiA0MC4zMjAzQzEzMy45MTEgNDAuNDA4OSAxMzMuNzA2IDQwLjUzOTEgMTMzLjUzOSA0MC43MTA5QzEzMy4zNzggNDAuODc3NiAxMzMuMjUzIDQxLjA4MDcgMTMzLjE2NCA0MS4zMjAzQzEzMy4wNzYgNDEuNTU5OSAxMzMuMDIzIDQxLjgyODEgMTMzLjAwOCA0Mi4xMjVMMTMyLjU3OCA0Mi4xNTYyQzEzMi41NzggNDEuNjI1IDEzMi42MyA0MS4xMzI4IDEzMi43MzQgNDAuNjc5N0MxMzIuODM5IDQwLjIyNjYgMTMyLjk5NSAzOS44MjgxIDEzMy4yMDMgMzkuNDg0NEMxMzMuNDE3IDM5LjE0MDYgMTMzLjY4MiAzOC44NzI0IDEzNCAzOC42Nzk3QzEzNC4zMjMgMzguNDg3IDEzNC42OTUgMzguMzkwNiAxMzUuMTE3IDM4LjM5MDZDMTM1LjIzMiAzOC4zOTA2IDEzNS4zNTQgMzguNDAxIDEzNS40ODQgMzguNDIxOUMxMzUuNjIgMzguNDQyNyAxMzUuNzIxIDM4LjQ2NjEgMTM1Ljc4OSAzOC40OTIyWk0xNDEuNzAzIDQ1LjMwNDdWNDEuMjczNEMxNDEuNzAzIDQwLjk3MTQgMTQxLjY0OCA0MC43MTA5IDE0MS41MzkgNDAuNDkyMkMxNDEuNDMgNDAuMjczNCAxNDEuMjYzIDQwLjEwNDIgMTQxLjAzOSAzOS45ODQ0QzE0MC44MiAzOS44NjQ2IDE0MC41NDQgMzkuODA0NyAxNDAuMjExIDM5LjgwNDdDMTM5LjkwNCAzOS44MDQ3IDEzOS42MzggMzkuODU2OCAxMzkuNDE0IDM5Ljk2MDlDMTM5LjE5IDQwLjA2NTEgMTM5LjAxNiA0MC4yMDU3IDEzOC44OTEgNDAuMzgyOEMxMzguNzY2IDQwLjU1OTkgMTM4LjcwMyA0MC43NjA0IDEzOC43MDMgNDAuOTg0NEgxMzYuODI4QzEzNi44MjggNDAuNjUxIDEzNi45MDkgNDAuMzI4MSAxMzcuMDcgNDAuMDE1NkMxMzcuMjMyIDM5LjcwMzEgMTM3LjQ2NiAzOS40MjQ1IDEzNy43NzMgMzkuMTc5N0MxMzguMDgxIDM4LjkzNDkgMTM4LjQ0OCAzOC43NDIyIDEzOC44NzUgMzguNjAxNkMxMzkuMzAyIDM4LjQ2MDkgMTM5Ljc4MSAzOC4zOTA2IDE0MC4zMTIgMzguMzkwNkMxNDAuOTQ4IDM4LjM5MDYgMTQxLjUxIDM4LjQ5NzQgMTQyIDM4LjcxMDlDMTQyLjQ5NSAzOC45MjQ1IDE0Mi44ODMgMzkuMjQ3NCAxNDMuMTY0IDM5LjY3OTdDMTQzLjQ1MSA0MC4xMDY4IDE0My41OTQgNDAuNjQzMiAxNDMuNTk0IDQxLjI4OTFWNDUuMDQ2OUMxNDMuNTk0IDQ1LjQzMjMgMTQzLjYyIDQ1Ljc3ODYgMTQzLjY3MiA0Ni4wODU5QzE0My43MjkgNDYuMzg4IDE0My44MSA0Ni42NTEgMTQzLjkxNCA0Ni44NzVWNDdIMTQxLjk4NEMxNDEuODk2IDQ2Ljc5NjkgMTQxLjgyNiA0Ni41MzkxIDE0MS43NzMgNDYuMjI2NkMxNDEuNzI3IDQ1LjkwODkgMTQxLjcwMyA0NS42MDE2IDE0MS43MDMgNDUuMzA0N1pNMTQxLjk3NyA0MS44NTk0TDE0MS45OTIgNDMuMDIzNEgxNDAuNjQxQzE0MC4yOTIgNDMuMDIzNCAxMzkuOTg0IDQzLjA1NzMgMTM5LjcxOSA0My4xMjVDMTM5LjQ1MyA0My4xODc1IDEzOS4yMzIgNDMuMjgxMiAxMzkuMDU1IDQzLjQwNjJDMTM4Ljg3OCA0My41MzEyIDEzOC43NDUgNDMuNjgyMyAxMzguNjU2IDQzLjg1OTRDMTM4LjU2OCA0NC4wMzY1IDEzOC41MjMgNDQuMjM3IDEzOC41MjMgNDQuNDYwOUMxMzguNTIzIDQ0LjY4NDkgMTM4LjU3NiA0NC44OTA2IDEzOC42OCA0NS4wNzgxQzEzOC43ODQgNDUuMjYwNCAxMzguOTM1IDQ1LjQwMzYgMTM5LjEzMyA0NS41MDc4QzEzOS4zMzYgNDUuNjEyIDEzOS41ODEgNDUuNjY0MSAxMzkuODY3IDQ1LjY2NDFDMTQwLjI1MyA0NS42NjQxIDE0MC41ODkgNDUuNTg1OSAxNDAuODc1IDQ1LjQyOTdDMTQxLjE2NyA0NS4yNjgyIDE0MS4zOTYgNDUuMDcyOSAxNDEuNTYyIDQ0Ljg0MzhDMTQxLjcyOSA0NC42MDk0IDE0MS44MTggNDQuMzg4IDE0MS44MjggNDQuMTc5N0wxNDIuNDM4IDQ1LjAxNTZDMTQyLjM3NSA0NS4yMjkyIDE0Mi4yNjggNDUuNDU4MyAxNDIuMTE3IDQ1LjcwMzFDMTQxLjk2NiA0NS45NDc5IDE0MS43NjggNDYuMTgyMyAxNDEuNTIzIDQ2LjQwNjJDMTQxLjI4NCA0Ni42MjUgMTQwLjk5NSA0Ni44MDQ3IDE0MC42NTYgNDYuOTQ1M0MxNDAuMzIzIDQ3LjA4NTkgMTM5LjkzOCA0Ny4xNTYyIDEzOS41IDQ3LjE1NjJDMTM4Ljk0OCA0Ny4xNTYyIDEzOC40NTYgNDcuMDQ2OSAxMzguMDIzIDQ2LjgyODFDMTM3LjU5MSA0Ni42MDQyIDEzNy4yNTMgNDYuMzA0NyAxMzcuMDA4IDQ1LjkyOTdDMTM2Ljc2MyA0NS41NDk1IDEzNi42NDEgNDUuMTE5OCAxMzYuNjQxIDQ0LjY0MDZDMTM2LjY0MSA0NC4xOTI3IDEzNi43MjQgNDMuNzk2OSAxMzYuODkxIDQzLjQ1MzFDMTM3LjA2MiA0My4xMDQyIDEzNy4zMTIgNDIuODEyNSAxMzcuNjQxIDQyLjU3ODFDMTM3Ljk3NCA0Mi4zNDM4IDEzOC4zOCA0Mi4xNjY3IDEzOC44NTkgNDIuMDQ2OUMxMzkuMzM5IDQxLjkyMTkgMTM5Ljg4NSA0MS44NTk0IDE0MC41IDQxLjg1OTRIMTQxLjk3N1pNMTQ5LjY4OCAzOC41NDY5VjM5LjkyMTlIMTQ0LjkyMlYzOC41NDY5SDE0OS42ODhaTTE0Ni4yOTcgMzYuNDc2NkgxNDguMThWNDQuNjY0MUMxNDguMTggNDQuOTI0NSAxNDguMjE2IDQ1LjEyNSAxNDguMjg5IDQ1LjI2NTZDMTQ4LjM2NyA0NS40MDEgMTQ4LjQ3NCA0NS40OTIyIDE0OC42MDkgNDUuNTM5MUMxNDguNzQ1IDQ1LjU4NTkgMTQ4LjkwNCA0NS42MDk0IDE0OS4wODYgNDUuNjA5NEMxNDkuMjE2IDQ1LjYwOTQgMTQ5LjM0MSA0NS42MDE2IDE0OS40NjEgNDUuNTg1OUMxNDkuNTgxIDQ1LjU3MDMgMTQ5LjY3NyA0NS41NTQ3IDE0OS43NSA0NS41MzkxTDE0OS43NTggNDYuOTc2NkMxNDkuNjAyIDQ3LjAyMzQgMTQ5LjQxOSA0Ny4wNjUxIDE0OS4yMTEgNDcuMTAxNkMxNDkuMDA4IDQ3LjEzOCAxNDguNzczIDQ3LjE1NjIgMTQ4LjUwOCA0Ny4xNTYyQzE0OC4wNzYgNDcuMTU2MiAxNDcuNjkzIDQ3LjA4MDcgMTQ3LjM1OSA0Ni45Mjk3QzE0Ny4wMjYgNDYuNzczNCAxNDYuNzY2IDQ2LjUyMDggMTQ2LjU3OCA0Ni4xNzE5QzE0Ni4zOTEgNDUuODIyOSAxNDYuMjk3IDQ1LjM1OTQgMTQ2LjI5NyA0NC43ODEyVjM2LjQ3NjZaTTE1Ni40NzcgNDUuMDA3OFYzOC41NDY5SDE1OC4zNjdWNDdIMTU2LjU4NkwxNTYuNDc3IDQ1LjAwNzhaTTE1Ni43NDIgNDMuMjVMMTU3LjM3NSA0My4yMzQ0QzE1Ny4zNzUgNDMuODAyMSAxNTcuMzEyIDQ0LjMyNTUgMTU3LjE4OCA0NC44MDQ3QzE1Ny4wNjIgNDUuMjc4NiAxNTYuODcgNDUuNjkyNyAxNTYuNjA5IDQ2LjA0NjlDMTU2LjM0OSA0Ni4zOTU4IDE1Ni4wMTYgNDYuNjY5MyAxNTUuNjA5IDQ2Ljg2NzJDMTU1LjIwMyA0Ny4wNTk5IDE1NC43MTYgNDcuMTU2MiAxNTQuMTQ4IDQ3LjE1NjJDMTUzLjczNyA0Ny4xNTYyIDE1My4zNTkgNDcuMDk2NCAxNTMuMDE2IDQ2Ljk3NjZDMTUyLjY3MiA0Ni44NTY4IDE1Mi4zNzUgNDYuNjcxOSAxNTIuMTI1IDQ2LjQyMTlDMTUxLjg4IDQ2LjE3MTkgMTUxLjY5IDQ1Ljg0NjQgMTUxLjU1NSA0NS40NDUzQzE1MS40MTkgNDUuMDQ0MyAxNTEuMzUyIDQ0LjU2NTEgMTUxLjM1MiA0NC4wMDc4VjM4LjU0NjlIMTUzLjIzNFY0NC4wMjM0QzE1My4yMzQgNDQuMzMwNyAxNTMuMjcxIDQ0LjU4ODUgMTUzLjM0NCA0NC43OTY5QzE1My40MTcgNDUgMTUzLjUxNiA0NS4xNjQxIDE1My42NDEgNDUuMjg5MUMxNTMuNzY2IDQ1LjQxNDEgMTUzLjkxMSA0NS41MDI2IDE1NC4wNzggNDUuNTU0N0MxNTQuMjQ1IDQ1LjYwNjggMTU0LjQyMiA0NS42MzI4IDE1NC42MDkgNDUuNjMyOEMxNTUuMTQ2IDQ1LjYzMjggMTU1LjU2OCA0NS41Mjg2IDE1NS44NzUgNDUuMzIwM0MxNTYuMTg4IDQ1LjEwNjggMTU2LjQwOSA0NC44MjAzIDE1Ni41MzkgNDQuNDYwOUMxNTYuNjc0IDQ0LjEwMTYgMTU2Ljc0MiA0My42OTc5IDE1Ni43NDIgNDMuMjVaTTE2Mi40MzggNDAuMTU2MlY0N0gxNjAuNTU1VjM4LjU0NjlIMTYyLjM1MkwxNjIuNDM4IDQwLjE1NjJaTTE2NS4wMjMgMzguNDkyMkwxNjUuMDA4IDQwLjI0MjJDMTY0Ljg5MyA0MC4yMjE0IDE2NC43NjggNDAuMjA1NyAxNjQuNjMzIDQwLjE5NTNDMTY0LjUwMyA0MC4xODQ5IDE2NC4zNzIgNDAuMTc5NyAxNjQuMjQyIDQwLjE3OTdDMTYzLjkxOSA0MC4xNzk3IDE2My42MzUgNDAuMjI2NiAxNjMuMzkxIDQwLjMyMDNDMTYzLjE0NiA0MC40MDg5IDE2Mi45NCA0MC41MzkxIDE2Mi43NzMgNDAuNzEwOUMxNjIuNjEyIDQwLjg3NzYgMTYyLjQ4NyA0MS4wODA3IDE2Mi4zOTggNDEuMzIwM0MxNjIuMzEgNDEuNTU5OSAxNjIuMjU4IDQxLjgyODEgMTYyLjI0MiA0Mi4xMjVMMTYxLjgxMiA0Mi4xNTYyQzE2MS44MTIgNDEuNjI1IDE2MS44NjUgNDEuMTMyOCAxNjEuOTY5IDQwLjY3OTdDMTYyLjA3MyA0MC4yMjY2IDE2Mi4yMjkgMzkuODI4MSAxNjIuNDM4IDM5LjQ4NDRDMTYyLjY1MSAzOS4xNDA2IDE2Mi45MTcgMzguODcyNCAxNjMuMjM0IDM4LjY3OTdDMTYzLjU1NyAzOC40ODcgMTYzLjkzIDM4LjM5MDYgMTY0LjM1MiAzOC4zOTA2QzE2NC40NjYgMzguMzkwNiAxNjQuNTg5IDM4LjQwMSAxNjQuNzE5IDM4LjQyMTlDMTY0Ljg1NCAzOC40NDI3IDE2NC45NTYgMzguNDY2MSAxNjUuMDIzIDM4LjQ5MjJaTTE3MC4wMjMgNDcuMTU2MkMxNjkuMzk4IDQ3LjE1NjIgMTY4LjgzMyA0Ny4wNTQ3IDE2OC4zMjggNDYuODUxNkMxNjcuODI4IDQ2LjY0MzIgMTY3LjQwMSA0Ni4zNTQyIDE2Ny4wNDcgNDUuOTg0NEMxNjYuNjk4IDQ1LjYxNDYgMTY2LjQzIDQ1LjE3OTcgMTY2LjI0MiA0NC42Nzk3QzE2Ni4wNTUgNDQuMTc5NyAxNjUuOTYxIDQzLjY0MDYgMTY1Ljk2MSA0My4wNjI1VjQyLjc1QzE2NS45NjEgNDIuMDg4NSAxNjYuMDU3IDQxLjQ4OTYgMTY2LjI1IDQwLjk1MzFDMTY2LjQ0MyA0MC40MTY3IDE2Ni43MTEgMzkuOTU4MyAxNjcuMDU1IDM5LjU3ODFDMTY3LjM5OCAzOS4xOTI3IDE2Ny44MDUgMzguODk4NCAxNjguMjczIDM4LjY5NTNDMTY4Ljc0MiAzOC40OTIyIDE2OS4yNSAzOC4zOTA2IDE2OS43OTcgMzguMzkwNkMxNzAuNDAxIDM4LjM5MDYgMTcwLjkzIDM4LjQ5MjIgMTcxLjM4MyAzOC42OTUzQzE3MS44MzYgMzguODk4NCAxNzIuMjExIDM5LjE4NDkgMTcyLjUwOCAzOS41NTQ3QzE3Mi44MSAzOS45MTkzIDE3My4wMzQgNDAuMzU0MiAxNzMuMTggNDAuODU5NEMxNzMuMzMxIDQxLjM2NDYgMTczLjQwNiA0MS45MjE5IDE3My40MDYgNDIuNTMxMlY0My4zMzU5SDE2Ni44NzVWNDEuOTg0NEgxNzEuNTQ3VjQxLjgzNTlDMTcxLjUzNiA0MS40OTc0IDE3MS40NjkgNDEuMTc5NyAxNzEuMzQ0IDQwLjg4MjhDMTcxLjIyNCA0MC41ODU5IDE3MS4wMzkgNDAuMzQ2NCAxNzAuNzg5IDQwLjE2NDFDMTcwLjUzOSAzOS45ODE4IDE3MC4yMDYgMzkuODkwNiAxNjkuNzg5IDM5Ljg5MDZDMTY5LjQ3NyAzOS44OTA2IDE2OS4xOTggMzkuOTU4MyAxNjguOTUzIDQwLjA5MzhDMTY4LjcxNCA0MC4yMjQgMTY4LjUxMyA0MC40MTQxIDE2OC4zNTIgNDAuNjY0MUMxNjguMTkgNDAuOTE0MSAxNjguMDY1IDQxLjIxNjEgMTY3Ljk3NyA0MS41NzAzQzE2Ny44OTMgNDEuOTE5MyAxNjcuODUyIDQyLjMxMjUgMTY3Ljg1MiA0Mi43NVY0My4wNjI1QzE2Ny44NTIgNDMuNDMyMyAxNjcuOTAxIDQzLjc3NiAxNjggNDQuMDkzOEMxNjguMTA0IDQ0LjQwNjIgMTY4LjI1NSA0NC42Nzk3IDE2OC40NTMgNDQuOTE0MUMxNjguNjUxIDQ1LjE0ODQgMTY4Ljg5MSA0NS4zMzMzIDE2OS4xNzIgNDUuNDY4OEMxNjkuNDUzIDQ1LjU5OSAxNjkuNzczIDQ1LjY2NDEgMTcwLjEzMyA0NS42NjQxQzE3MC41ODYgNDUuNjY0MSAxNzAuOTkgNDUuNTcyOSAxNzEuMzQ0IDQ1LjM5MDZDMTcxLjY5OCA0NS4yMDgzIDE3Mi4wMDUgNDQuOTUwNSAxNzIuMjY2IDQ0LjYxNzJMMTczLjI1OCA0NS41NzgxQzE3My4wNzYgNDUuODQzOCAxNzIuODM5IDQ2LjA5OSAxNzIuNTQ3IDQ2LjM0MzhDMTcyLjI1NSA0Ni41ODMzIDE3MS44OTggNDYuNzc4NiAxNzEuNDc3IDQ2LjkyOTdDMTcxLjA2IDQ3LjA4MDcgMTcwLjU3NiA0Ny4xNTYyIDE3MC4wMjMgNDcuMTU2MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg2LjIxMDkgNjQuODM0VjY2SDgxLjkyNzdWNjQuODM0SDg2LjIxMDlaTTgyLjMzNzkgNTcuNDY4OFY2Nkg4MC44NjcyVjU3LjQ2ODhIODIuMzM3OVpNOTEuMDMxMiA2NC43Mjg1VjYxLjcwNTFDOTEuMDMxMiA2MS40Nzg1IDkwLjk5MDIgNjEuMjgzMiA5MC45MDgyIDYxLjExOTFDOTAuODI2MiA2MC45NTUxIDkwLjcwMTIgNjAuODI4MSA5MC41MzMyIDYwLjczODNDOTAuMzY5MSA2MC42NDg0IDkwLjE2MjEgNjAuNjAzNSA4OS45MTIxIDYwLjYwMzVDODkuNjgxNiA2MC42MDM1IDg5LjQ4MjQgNjAuNjQyNiA4OS4zMTQ1IDYwLjcyMDdDODkuMTQ2NSA2MC43OTg4IDg5LjAxNTYgNjAuOTA0MyA4OC45MjE5IDYxLjAzNzFDODguODI4MSA2MS4xNjk5IDg4Ljc4MTIgNjEuMzIwMyA4OC43ODEyIDYxLjQ4ODNIODcuMzc1Qzg3LjM3NSA2MS4yMzgzIDg3LjQzNTUgNjAuOTk2MSA4Ny41NTY2IDYwLjc2MTdDODcuNjc3NyA2MC41MjczIDg3Ljg1MzUgNjAuMzE4NCA4OC4wODQgNjAuMTM0OEM4OC4zMTQ1IDU5Ljk1MTIgODguNTg5OCA1OS44MDY2IDg4LjkxMDIgNTkuNzAxMkM4OS4yMzA1IDU5LjU5NTcgODkuNTg5OCA1OS41NDMgODkuOTg4MyA1OS41NDNDOTAuNDY0OCA1OS41NDMgOTAuODg2NyA1OS42MjMgOTEuMjUzOSA1OS43ODMyQzkxLjYyNSA1OS45NDM0IDkxLjkxNiA2MC4xODU1IDkyLjEyNyA2MC41MDk4QzkyLjM0MTggNjAuODMwMSA5Mi40NDkyIDYxLjIzMjQgOTIuNDQ5MiA2MS43MTY4VjY0LjUzNTJDOTIuNDQ5MiA2NC44MjQyIDkyLjQ2ODggNjUuMDg0IDkyLjUwNzggNjUuMzE0NUM5Mi41NTA4IDY1LjU0MSA5Mi42MTEzIDY1LjczODMgOTIuNjg5NSA2NS45MDYyVjY2SDkxLjI0MjJDOTEuMTc1OCA2NS44NDc3IDkxLjEyMyA2NS42NTQzIDkxLjA4NCA2NS40MTk5QzkxLjA0ODggNjUuMTgxNiA5MS4wMzEyIDY0Ljk1MTIgOTEuMDMxMiA2NC43Mjg1Wk05MS4yMzYzIDYyLjE0NDVMOTEuMjQ4IDYzLjAxNzZIOTAuMjM0NEM4OS45NzI3IDYzLjAxNzYgODkuNzQyMiA2My4wNDMgODkuNTQzIDYzLjA5MzhDODkuMzQzOCA2My4xNDA2IDg5LjE3NzcgNjMuMjEwOSA4OS4wNDQ5IDYzLjMwNDdDODguOTEyMSA2My4zOTg0IDg4LjgxMjUgNjMuNTExNyA4OC43NDYxIDYzLjY0NDVDODguNjc5NyA2My43NzczIDg4LjY0NjUgNjMuOTI3NyA4OC42NDY1IDY0LjA5NTdDODguNjQ2NSA2NC4yNjM3IDg4LjY4NTUgNjQuNDE4IDg4Ljc2MzcgNjQuNTU4NkM4OC44NDE4IDY0LjY5NTMgODguOTU1MSA2NC44MDI3IDg5LjEwMzUgNjQuODgwOUM4OS4yNTU5IDY0Ljk1OSA4OS40Mzk1IDY0Ljk5OCA4OS42NTQzIDY0Ljk5OEM4OS45NDM0IDY0Ljk5OCA5MC4xOTUzIDY0LjkzOTUgOTAuNDEwMiA2NC44MjIzQzkwLjYyODkgNjQuNzAxMiA5MC44MDA4IDY0LjU1NDcgOTAuOTI1OCA2NC4zODI4QzkxLjA1MDggNjQuMjA3IDkxLjExNzIgNjQuMDQxIDkxLjEyNSA2My44ODQ4TDkxLjU4MiA2NC41MTE3QzkxLjUzNTIgNjQuNjcxOSA5MS40NTUxIDY0Ljg0MzggOTEuMzQxOCA2NS4wMjczQzkxLjIyODUgNjUuMjEwOSA5MS4wODAxIDY1LjM4NjcgOTAuODk2NSA2NS41NTQ3QzkwLjcxNjggNjUuNzE4OCA5MC41IDY1Ljg1MzUgOTAuMjQ2MSA2NS45NTlDODkuOTk2MSA2Ni4wNjQ1IDg5LjcwNyA2Ni4xMTcyIDg5LjM3ODkgNjYuMTE3MkM4OC45NjQ4IDY2LjExNzIgODguNTk1NyA2Ni4wMzUyIDg4LjI3MTUgNjUuODcxMUM4Ny45NDczIDY1LjcwMzEgODcuNjkzNCA2NS40Nzg1IDg3LjUwOTggNjUuMTk3M0M4Ny4zMjYyIDY0LjkxMjEgODcuMjM0NCA2NC41ODk4IDg3LjIzNDQgNjQuMjMwNUM4Ny4yMzQ0IDYzLjg5NDUgODcuMjk2OSA2My41OTc3IDg3LjQyMTkgNjMuMzM5OEM4Ny41NTA4IDYzLjA3ODEgODcuNzM4MyA2Mi44NTk0IDg3Ljk4NDQgNjIuNjgzNkM4OC4yMzQ0IDYyLjUwNzggODguNTM5MSA2Mi4zNzUgODguODk4NCA2Mi4yODUyQzg5LjI1NzggNjIuMTkxNCA4OS42NjggNjIuMTQ0NSA5MC4xMjg5IDYyLjE0NDVIOTEuMjM2M1pNOTcuNzMyNCA2NC4yODMyQzk3LjczMjQgNjQuMTQyNiA5Ny42OTczIDY0LjAxNTYgOTcuNjI3IDYzLjkwMjNDOTcuNTU2NiA2My43ODUyIDk3LjQyMTkgNjMuNjc5NyA5Ny4yMjI3IDYzLjU4NTlDOTcuMDI3MyA2My40OTIyIDk2LjczODMgNjMuNDA2MiA5Ni4zNTU1IDYzLjMyODFDOTYuMDE5NSA2My4yNTM5IDk1LjcxMDkgNjMuMTY2IDk1LjQyOTcgNjMuMDY0NUM5NS4xNTIzIDYyLjk1OSA5NC45MTQxIDYyLjgzMiA5NC43MTQ4IDYyLjY4MzZDOTQuNTE1NiA2Mi41MzUyIDk0LjM2MTMgNjIuMzU5NCA5NC4yNTIgNjIuMTU2MkM5NC4xNDI2IDYxLjk1MzEgOTQuMDg3OSA2MS43MTg4IDk0LjA4NzkgNjEuNDUzMUM5NC4wODc5IDYxLjE5NTMgOTQuMTQ0NSA2MC45NTEyIDk0LjI1NzggNjAuNzIwN0M5NC4zNzExIDYwLjQ5MDIgOTQuNTMzMiA2MC4yODcxIDk0Ljc0NDEgNjAuMTExM0M5NC45NTUxIDU5LjkzNTUgOTUuMjEwOSA1OS43OTY5IDk1LjUxMTcgNTkuNjk1M0M5NS44MTY0IDU5LjU5MzggOTYuMTU2MiA1OS41NDMgOTYuNTMxMiA1OS41NDNDOTcuMDYyNSA1OS41NDMgOTcuNTE3NiA1OS42MzI4IDk3Ljg5NjUgNTkuODEyNUM5OC4yNzkzIDU5Ljk4ODMgOTguNTcyMyA2MC4yMjg1IDk4Ljc3NTQgNjAuNTMzMkM5OC45Nzg1IDYwLjgzNCA5OS4wODAxIDYxLjE3MzggOTkuMDgwMSA2MS41NTI3SDk3LjY2OEM5Ny42NjggNjEuMzg0OCA5Ny42MjUgNjEuMjI4NSA5Ny41MzkxIDYxLjA4NEM5Ny40NTcgNjAuOTM1NSA5Ny4zMzIgNjAuODE2NCA5Ny4xNjQxIDYwLjcyNjZDOTYuOTk2MSA2MC42MzI4IDk2Ljc4NTIgNjAuNTg1OSA5Ni41MzEyIDYwLjU4NTlDOTYuMjg5MSA2MC41ODU5IDk2LjA4NzkgNjAuNjI1IDk1LjkyNzcgNjAuNzAzMUM5NS43NzE1IDYwLjc3NzMgOTUuNjU0MyA2MC44NzUgOTUuNTc2MiA2MC45OTYxQzk1LjUwMiA2MS4xMTcyIDk1LjQ2NDggNjEuMjUgOTUuNDY0OCA2MS4zOTQ1Qzk1LjQ2NDggNjEuNSA5NS40ODQ0IDYxLjU5NTcgOTUuNTIzNCA2MS42ODE2Qzk1LjU2NjQgNjEuNzYzNyA5NS42MzY3IDYxLjgzOTggOTUuNzM0NCA2MS45MTAyQzk1LjgzMiA2MS45NzY2IDk1Ljk2NDggNjIuMDM5MSA5Ni4xMzI4IDYyLjA5NzdDOTYuMzA0NyA2Mi4xNTYyIDk2LjUxOTUgNjIuMjEyOSA5Ni43NzczIDYyLjI2NzZDOTcuMjYxNyA2Mi4zNjkxIDk3LjY3NzcgNjIuNSA5OC4wMjU0IDYyLjY2MDJDOTguMzc3IDYyLjgxNjQgOTguNjQ2NSA2My4wMTk1IDk4LjgzNCA2My4yNjk1Qzk5LjAyMTUgNjMuNTE1NiA5OS4xMTUyIDYzLjgyODEgOTkuMTE1MiA2NC4yMDdDOTkuMTE1MiA2NC40ODgzIDk5LjA1NDcgNjQuNzQ2MSA5OC45MzM2IDY0Ljk4MDVDOTguODE2NCA2NS4yMTA5IDk4LjY0NDUgNjUuNDEyMSA5OC40MTggNjUuNTg0Qzk4LjE5MTQgNjUuNzUyIDk3LjkxOTkgNjUuODgyOCA5Ny42MDM1IDY1Ljk3NjZDOTcuMjkxIDY2LjA3MDMgOTYuOTM5NSA2Ni4xMTcyIDk2LjU0ODggNjYuMTE3MkM5NS45NzQ2IDY2LjExNzIgOTUuNDg4MyA2Ni4wMTU2IDk1LjA4OTggNjUuODEyNUM5NC42OTE0IDY1LjYwNTUgOTQuMzg4NyA2NS4zNDE4IDk0LjE4MTYgNjUuMDIxNUM5My45Nzg1IDY0LjY5NzMgOTMuODc3IDY0LjM2MTMgOTMuODc3IDY0LjAxMzdIOTUuMjQyMkM5NS4yNTc4IDY0LjI3NTQgOTUuMzMwMSA2NC40ODQ0IDk1LjQ1OSA2NC42NDA2Qzk1LjU5MTggNjQuNzkzIDk1Ljc1NTkgNjQuOTA0MyA5NS45NTEyIDY0Ljk3NDZDOTYuMTUwNCA2NS4wNDEgOTYuMzU1NSA2NS4wNzQyIDk2LjU2NjQgNjUuMDc0MkM5Ni44MjAzIDY1LjA3NDIgOTcuMDMzMiA2NS4wNDEgOTcuMjA1MSA2NC45NzQ2Qzk3LjM3NyA2NC45MDQzIDk3LjUwNzggNjQuODEwNSA5Ny41OTc3IDY0LjY5MzRDOTcuNjg3NSA2NC41NzIzIDk3LjczMjQgNjQuNDM1NSA5Ny43MzI0IDY0LjI4MzJaTTEwMy41MDggNTkuNjYwMlY2MC42OTE0SDk5LjkzMzZWNTkuNjYwMkgxMDMuNTA4Wk0xMDAuOTY1IDU4LjEwNzRIMTAyLjM3N1Y2NC4yNDhDMTAyLjM3NyA2NC40NDM0IDEwMi40MDQgNjQuNTkzOCAxMDIuNDU5IDY0LjY5OTJDMTAyLjUxOCA2NC44MDA4IDEwMi41OTggNjQuODY5MSAxMDIuNjk5IDY0LjkwNDNDMTAyLjgwMSA2NC45Mzk1IDEwMi45MiA2NC45NTcgMTAzLjA1NyA2NC45NTdDMTAzLjE1NCA2NC45NTcgMTAzLjI0OCA2NC45NTEyIDEwMy4zMzggNjQuOTM5NUMxMDMuNDI4IDY0LjkyNzcgMTAzLjUgNjQuOTE2IDEwMy41NTUgNjQuOTA0M0wxMDMuNTYxIDY1Ljk4MjRDMTAzLjQ0MyA2Ni4wMTc2IDEwMy4zMDcgNjYuMDQ4OCAxMDMuMTUgNjYuMDc2MkMxMDIuOTk4IDY2LjEwMzUgMTAyLjgyMiA2Ni4xMTcyIDEwMi42MjMgNjYuMTE3MkMxMDIuMjk5IDY2LjExNzIgMTAyLjAxMiA2Ni4wNjA1IDEwMS43NjIgNjUuOTQ3M0MxMDEuNTEyIDY1LjgzMDEgMTAxLjMxNiA2NS42NDA2IDEwMS4xNzYgNjUuMzc4OUMxMDEuMDM1IDY1LjExNzIgMTAwLjk2NSA2NC43Njk1IDEwMC45NjUgNjQuMzM1OVY1OC4xMDc0Wk0xMTEuOSA2NC41MDU5VjU5LjY2MDJIMTEzLjMxOFY2NkgxMTEuOTgyTDExMS45IDY0LjUwNTlaTTExMi4xIDYzLjE4NzVMMTEyLjU3NCA2My4xNzU4QzExMi41NzQgNjMuNjAxNiAxMTIuNTI3IDYzLjk5NDEgMTEyLjQzNCA2NC4zNTM1QzExMi4zNCA2NC43MDkgMTEyLjE5NSA2NS4wMTk1IDExMiA2NS4yODUyQzExMS44MDUgNjUuNTQ2OSAxMTEuNTU1IDY1Ljc1MiAxMTEuMjUgNjUuOTAwNEMxMTAuOTQ1IDY2LjA0NDkgMTEwLjU4IDY2LjExNzIgMTEwLjE1NCA2Ni4xMTcyQzEwOS44NDYgNjYuMTE3MiAxMDkuNTYyIDY2LjA3MjMgMTA5LjMwNSA2NS45ODI0QzEwOS4wNDcgNjUuODkyNiAxMDguODI0IDY1Ljc1MzkgMTA4LjYzNyA2NS41NjY0QzEwOC40NTMgNjUuMzc4OSAxMDguMzExIDY1LjEzNDggMTA4LjIwOSA2NC44MzRDMTA4LjEwNyA2NC41MzMyIDEwOC4wNTcgNjQuMTczOCAxMDguMDU3IDYzLjc1NTlWNTkuNjYwMkgxMDkuNDY5VjYzLjc2NzZDMTA5LjQ2OSA2My45OTggMTA5LjQ5NiA2NC4xOTE0IDEwOS41NTEgNjQuMzQ3N0MxMDkuNjA1IDY0LjUgMTA5LjY4IDY0LjYyMyAxMDkuNzczIDY0LjcxNjhDMTA5Ljg2NyA2NC44MTA1IDEwOS45NzcgNjQuODc3IDExMC4xMDIgNjQuOTE2QzExMC4yMjcgNjQuOTU1MSAxMTAuMzU5IDY0Ljk3NDYgMTEwLjUgNjQuOTc0NkMxMTAuOTAyIDY0Ljk3NDYgMTExLjIxOSA2NC44OTY1IDExMS40NDkgNjQuNzQwMkMxMTEuNjg0IDY0LjU4MDEgMTExLjg1IDY0LjM2NTIgMTExLjk0NyA2NC4wOTU3QzExMi4wNDkgNjMuODI2MiAxMTIuMSA2My41MjM0IDExMi4xIDYzLjE4NzVaTTExNi40MzQgNjAuODc4OVY2OC40Mzc1SDExNS4wMjFWNTkuNjYwMkgxMTYuMzIyTDExNi40MzQgNjAuODc4OVpNMTIwLjU2NCA2Mi43NzE1VjYyLjg5NDVDMTIwLjU2NCA2My4zNTU1IDEyMC41MSA2My43ODMyIDEyMC40IDY0LjE3NzdDMTIwLjI5NSA2NC41Njg0IDEyMC4xMzcgNjQuOTEwMiAxMTkuOTI2IDY1LjIwMzFDMTE5LjcxOSA2NS40OTIyIDExOS40NjMgNjUuNzE2OCAxMTkuMTU4IDY1Ljg3N0MxMTguODU0IDY2LjAzNzEgMTE4LjUwMiA2Ni4xMTcyIDExOC4xMDQgNjYuMTE3MkMxMTcuNzA5IDY2LjExNzIgMTE3LjM2MyA2Ni4wNDQ5IDExNy4wNjYgNjUuOTAwNEMxMTYuNzczIDY1Ljc1MiAxMTYuNTI1IDY1LjU0MyAxMTYuMzIyIDY1LjI3MzRDMTE2LjExOSA2NS4wMDM5IDExNS45NTUgNjQuNjg3NSAxMTUuODMgNjQuMzI0MkMxMTUuNzA5IDYzLjk1NyAxMTUuNjIzIDYzLjU1NDcgMTE1LjU3MiA2My4xMTcyVjYyLjY0MjZDMTE1LjYyMyA2Mi4xNzc3IDExNS43MDkgNjEuNzU1OSAxMTUuODMgNjEuMzc3QzExNS45NTUgNjAuOTk4IDExNi4xMTkgNjAuNjcxOSAxMTYuMzIyIDYwLjM5ODRDMTE2LjUyNSA2MC4xMjUgMTE2Ljc3MyA1OS45MTQxIDExNy4wNjYgNTkuNzY1NkMxMTcuMzU5IDU5LjYxNzIgMTE3LjcwMSA1OS41NDMgMTE4LjA5MiA1OS41NDNDMTE4LjQ5IDU5LjU0MyAxMTguODQ0IDU5LjYyMTEgMTE5LjE1MiA1OS43NzczQzExOS40NjEgNTkuOTI5NyAxMTkuNzIxIDYwLjE0ODQgMTE5LjkzMiA2MC40MzM2QzEyMC4xNDMgNjAuNzE0OCAxMjAuMzAxIDYxLjA1NDcgMTIwLjQwNiA2MS40NTMxQzEyMC41MTIgNjEuODQ3NyAxMjAuNTY0IDYyLjI4NzEgMTIwLjU2NCA2Mi43NzE1Wk0xMTkuMTUyIDYyLjg5NDVWNjIuNzcxNUMxMTkuMTUyIDYyLjQ3ODUgMTE5LjEyNSA2Mi4yMDcgMTE5LjA3IDYxLjk1N0MxMTkuMDE2IDYxLjcwMzEgMTE4LjkzIDYxLjQ4MDUgMTE4LjgxMiA2MS4yODkxQzExOC42OTUgNjEuMDk3NyAxMTguNTQ1IDYwLjk0OTIgMTE4LjM2MSA2MC44NDM4QzExOC4xODIgNjAuNzM0NCAxMTcuOTY1IDYwLjY3OTcgMTE3LjcxMSA2MC42Nzk3QzExNy40NjEgNjAuNjc5NyAxMTcuMjQ2IDYwLjcyMjcgMTE3LjA2NiA2MC44MDg2QzExNi44ODcgNjAuODkwNiAxMTYuNzM2IDYxLjAwNTkgMTE2LjYxNSA2MS4xNTQzQzExNi40OTQgNjEuMzAyNyAxMTYuNCA2MS40NzY2IDExNi4zMzQgNjEuNjc1OEMxMTYuMjY4IDYxLjg3MTEgMTE2LjIyMSA2Mi4wODQgMTE2LjE5MyA2Mi4zMTQ1VjYzLjQ1MTJDMTE2LjI0IDYzLjczMjQgMTE2LjMyIDYzLjk5MDIgMTE2LjQzNCA2NC4yMjQ2QzExNi41NDcgNjQuNDU5IDExNi43MDcgNjQuNjQ2NSAxMTYuOTE0IDY0Ljc4NzFDMTE3LjEyNSA2NC45MjM4IDExNy4zOTUgNjQuOTkyMiAxMTcuNzIzIDY0Ljk5MjJDMTE3Ljk3NyA2NC45OTIyIDExOC4xOTMgNjQuOTM3NSAxMTguMzczIDY0LjgyODFDMTE4LjU1MyA2NC43MTg4IDExOC42OTkgNjQuNTY4NCAxMTguODEyIDY0LjM3N0MxMTguOTMgNjQuMTgxNiAxMTkuMDE2IDYzLjk1NyAxMTkuMDcgNjMuNzAzMUMxMTkuMTI1IDYzLjQ0OTIgMTE5LjE1MiA2My4xNzk3IDExOS4xNTIgNjIuODk0NVpNMTI1Ljg4MyA2NC42ODc1VjU3SDEyNy4zMDFWNjZIMTI2LjAxOEwxMjUuODgzIDY0LjY4NzVaTTEyMS43NTggNjIuOTAwNFY2Mi43NzczQzEyMS43NTggNjIuMjk2OSAxMjEuODE0IDYxLjg1OTQgMTIxLjkyOCA2MS40NjQ4QzEyMi4wNDEgNjEuMDY2NCAxMjIuMjA1IDYwLjcyNDYgMTIyLjQyIDYwLjQzOTVDMTIyLjYzNSA2MC4xNTA0IDEyMi44OTYgNTkuOTI5NyAxMjMuMjA1IDU5Ljc3NzNDMTIzLjUxNCA1OS42MjExIDEyMy44NjEgNTkuNTQzIDEyNC4yNDggNTkuNTQzQzEyNC42MzEgNTkuNTQzIDEyNC45NjcgNTkuNjE3MiAxMjUuMjU2IDU5Ljc2NTZDMTI1LjU0NSA1OS45MTQxIDEyNS43OTEgNjAuMTI3IDEyNS45OTQgNjAuNDA0M0MxMjYuMTk3IDYwLjY3NzcgMTI2LjM1OSA2MS4wMDU5IDEyNi40OCA2MS4zODg3QzEyNi42MDIgNjEuNzY3NiAxMjYuNjg4IDYyLjE4OTUgMTI2LjczOCA2Mi42NTQzVjYzLjA0NjlDMTI2LjY4OCA2My41IDEyNi42MDIgNjMuOTE0MSAxMjYuNDggNjQuMjg5MUMxMjYuMzU5IDY0LjY2NDEgMTI2LjE5NyA2NC45ODgzIDEyNS45OTQgNjUuMjYxN0MxMjUuNzkxIDY1LjUzNTIgMTI1LjU0MyA2NS43NDYxIDEyNS4yNSA2NS44OTQ1QzEyNC45NjEgNjYuMDQzIDEyNC42MjMgNjYuMTE3MiAxMjQuMjM2IDY2LjExNzJDMTIzLjg1NCA2Ni4xMTcyIDEyMy41MDggNjYuMDM3MSAxMjMuMTk5IDY1Ljg3N0MxMjIuODk1IDY1LjcxNjggMTIyLjYzNSA2NS40OTIyIDEyMi40MiA2NS4yMDMxQzEyMi4yMDUgNjQuOTE0MSAxMjIuMDQxIDY0LjU3NDIgMTIxLjkyOCA2NC4xODM2QzEyMS44MTQgNjMuNzg5MSAxMjEuNzU4IDYzLjM2MTMgMTIxLjc1OCA2Mi45MDA0Wk0xMjMuMTcgNjIuNzc3M1Y2Mi45MDA0QzEyMy4xNyA2My4xODk1IDEyMy4xOTUgNjMuNDU5IDEyMy4yNDYgNjMuNzA5QzEyMy4zMDEgNjMuOTU5IDEyMy4zODUgNjQuMTc5NyAxMjMuNDk4IDY0LjM3MTFDMTIzLjYxMSA2NC41NTg2IDEyMy43NTggNjQuNzA3IDEyMy45MzggNjQuODE2NEMxMjQuMTIxIDY0LjkyMTkgMTI0LjM0IDY0Ljk3NDYgMTI0LjU5NCA2NC45NzQ2QzEyNC45MTQgNjQuOTc0NiAxMjUuMTc4IDY0LjkwNDMgMTI1LjM4NSA2NC43NjM3QzEyNS41OTIgNjQuNjIzIDEyNS43NTQgNjQuNDMzNiAxMjUuODcxIDY0LjE5NTNDMTI1Ljk5MiA2My45NTMxIDEyNi4wNzQgNjMuNjgzNiAxMjYuMTE3IDYzLjM4NjdWNjIuMzI2MkMxMjYuMDk0IDYyLjA5NTcgMTI2LjA0NSA2MS44ODA5IDEyNS45NzEgNjEuNjgxNkMxMjUuOSA2MS40ODI0IDEyNS44MDUgNjEuMzA4NiAxMjUuNjg0IDYxLjE2MDJDMTI1LjU2MiA2MS4wMDc4IDEyNS40MTIgNjAuODkwNiAxMjUuMjMyIDYwLjgwODZDMTI1LjA1NyA2MC43MjI3IDEyNC44NDggNjAuNjc5NyAxMjQuNjA1IDYwLjY3OTdDMTI0LjM0OCA2MC42Nzk3IDEyNC4xMjkgNjAuNzM0NCAxMjMuOTQ5IDYwLjg0MzhDMTIzLjc3IDYwLjk1MzEgMTIzLjYyMSA2MS4xMDM1IDEyMy41MDQgNjEuMjk0OUMxMjMuMzkxIDYxLjQ4NjMgMTIzLjMwNyA2MS43MDkgMTIzLjI1MiA2MS45NjI5QzEyMy4xOTcgNjIuMjE2OCAxMjMuMTcgNjIuNDg4MyAxMjMuMTcgNjIuNzc3M1pNMTMyLjYwMiA2NC43Mjg1VjYxLjcwNTFDMTMyLjYwMiA2MS40Nzg1IDEzMi41NjEgNjEuMjgzMiAxMzIuNDc5IDYxLjExOTFDMTMyLjM5NiA2MC45NTUxIDEzMi4yNzEgNjAuODI4MSAxMzIuMTA0IDYwLjczODNDMTMxLjkzOSA2MC42NDg0IDEzMS43MzIgNjAuNjAzNSAxMzEuNDgyIDYwLjYwMzVDMTMxLjI1MiA2MC42MDM1IDEzMS4wNTMgNjAuNjQyNiAxMzAuODg1IDYwLjcyMDdDMTMwLjcxNyA2MC43OTg4IDEzMC41ODYgNjAuOTA0MyAxMzAuNDkyIDYxLjAzNzFDMTMwLjM5OCA2MS4xNjk5IDEzMC4zNTIgNjEuMzIwMyAxMzAuMzUyIDYxLjQ4ODNIMTI4Ljk0NUMxMjguOTQ1IDYxLjIzODMgMTI5LjAwNiA2MC45OTYxIDEyOS4xMjcgNjAuNzYxN0MxMjkuMjQ4IDYwLjUyNzMgMTI5LjQyNCA2MC4zMTg0IDEyOS42NTQgNjAuMTM0OEMxMjkuODg1IDU5Ljk1MTIgMTMwLjE2IDU5LjgwNjYgMTMwLjQ4IDU5LjcwMTJDMTMwLjgwMSA1OS41OTU3IDEzMS4xNiA1OS41NDMgMTMxLjU1OSA1OS41NDNDMTMyLjAzNSA1OS41NDMgMTMyLjQ1NyA1OS42MjMgMTMyLjgyNCA1OS43ODMyQzEzMy4xOTUgNTkuOTQzNCAxMzMuNDg2IDYwLjE4NTUgMTMzLjY5NyA2MC41MDk4QzEzMy45MTIgNjAuODMwMSAxMzQuMDIgNjEuMjMyNCAxMzQuMDIgNjEuNzE2OFY2NC41MzUyQzEzNC4wMiA2NC44MjQyIDEzNC4wMzkgNjUuMDg0IDEzNC4wNzggNjUuMzE0NUMxMzQuMTIxIDY1LjU0MSAxMzQuMTgyIDY1LjczODMgMTM0LjI2IDY1LjkwNjJWNjZIMTMyLjgxMkMxMzIuNzQ2IDY1Ljg0NzcgMTMyLjY5MyA2NS42NTQzIDEzMi42NTQgNjUuNDE5OUMxMzIuNjE5IDY1LjE4MTYgMTMyLjYwMiA2NC45NTEyIDEzMi42MDIgNjQuNzI4NVpNMTMyLjgwNyA2Mi4xNDQ1TDEzMi44MTggNjMuMDE3NkgxMzEuODA1QzEzMS41NDMgNjMuMDE3NiAxMzEuMzEyIDYzLjA0MyAxMzEuMTEzIDYzLjA5MzhDMTMwLjkxNCA2My4xNDA2IDEzMC43NDggNjMuMjEwOSAxMzAuNjE1IDYzLjMwNDdDMTMwLjQ4MiA2My4zOTg0IDEzMC4zODMgNjMuNTExNyAxMzAuMzE2IDYzLjY0NDVDMTMwLjI1IDYzLjc3NzMgMTMwLjIxNyA2My45Mjc3IDEzMC4yMTcgNjQuMDk1N0MxMzAuMjE3IDY0LjI2MzcgMTMwLjI1NiA2NC40MTggMTMwLjMzNCA2NC41NTg2QzEzMC40MTIgNjQuNjk1MyAxMzAuNTI1IDY0LjgwMjcgMTMwLjY3NCA2NC44ODA5QzEzMC44MjYgNjQuOTU5IDEzMS4wMSA2NC45OTggMTMxLjIyNSA2NC45OThDMTMxLjUxNCA2NC45OTggMTMxLjc2NiA2NC45Mzk1IDEzMS45OCA2NC44MjIzQzEzMi4xOTkgNjQuNzAxMiAxMzIuMzcxIDY0LjU1NDcgMTMyLjQ5NiA2NC4zODI4QzEzMi42MjEgNjQuMjA3IDEzMi42ODggNjQuMDQxIDEzMi42OTUgNjMuODg0OEwxMzMuMTUyIDY0LjUxMTdDMTMzLjEwNSA2NC42NzE5IDEzMy4wMjUgNjQuODQzOCAxMzIuOTEyIDY1LjAyNzNDMTMyLjc5OSA2NS4yMTA5IDEzMi42NSA2NS4zODY3IDEzMi40NjcgNjUuNTU0N0MxMzIuMjg3IDY1LjcxODggMTMyLjA3IDY1Ljg1MzUgMTMxLjgxNiA2NS45NTlDMTMxLjU2NiA2Ni4wNjQ1IDEzMS4yNzcgNjYuMTE3MiAxMzAuOTQ5IDY2LjExNzJDMTMwLjUzNSA2Ni4xMTcyIDEzMC4xNjYgNjYuMDM1MiAxMjkuODQyIDY1Ljg3MTFDMTI5LjUxOCA2NS43MDMxIDEyOS4yNjQgNjUuNDc4NSAxMjkuMDggNjUuMTk3M0MxMjguODk2IDY0LjkxMjEgMTI4LjgwNSA2NC41ODk4IDEyOC44MDUgNjQuMjMwNUMxMjguODA1IDYzLjg5NDUgMTI4Ljg2NyA2My41OTc3IDEyOC45OTIgNjMuMzM5OEMxMjkuMTIxIDYzLjA3ODEgMTI5LjMwOSA2Mi44NTk0IDEyOS41NTUgNjIuNjgzNkMxMjkuODA1IDYyLjUwNzggMTMwLjEwOSA2Mi4zNzUgMTMwLjQ2OSA2Mi4yODUyQzEzMC44MjggNjIuMTkxNCAxMzEuMjM4IDYyLjE0NDUgMTMxLjY5OSA2Mi4xNDQ1SDEzMi44MDdaTTEzOC42NTIgNTkuNjYwMlY2MC42OTE0SDEzNS4wNzhWNTkuNjYwMkgxMzguNjUyWk0xMzYuMTA5IDU4LjEwNzRIMTM3LjUyMVY2NC4yNDhDMTM3LjUyMSA2NC40NDM0IDEzNy41NDkgNjQuNTkzOCAxMzcuNjA0IDY0LjY5OTJDMTM3LjY2MiA2NC44MDA4IDEzNy43NDIgNjQuODY5MSAxMzcuODQ0IDY0LjkwNDNDMTM3Ljk0NSA2NC45Mzk1IDEzOC4wNjQgNjQuOTU3IDEzOC4yMDEgNjQuOTU3QzEzOC4yOTkgNjQuOTU3IDEzOC4zOTMgNjQuOTUxMiAxMzguNDgyIDY0LjkzOTVDMTM4LjU3MiA2NC45Mjc3IDEzOC42NDUgNjQuOTE2IDEzOC42OTkgNjQuOTA0M0wxMzguNzA1IDY1Ljk4MjRDMTM4LjU4OCA2Ni4wMTc2IDEzOC40NTEgNjYuMDQ4OCAxMzguMjk1IDY2LjA3NjJDMTM4LjE0MyA2Ni4xMDM1IDEzNy45NjcgNjYuMTE3MiAxMzcuNzY4IDY2LjExNzJDMTM3LjQ0MyA2Ni4xMTcyIDEzNy4xNTYgNjYuMDYwNSAxMzYuOTA2IDY1Ljk0NzNDMTM2LjY1NiA2NS44MzAxIDEzNi40NjEgNjUuNjQwNiAxMzYuMzIgNjUuMzc4OUMxMzYuMTggNjUuMTE3MiAxMzYuMTA5IDY0Ljc2OTUgMTM2LjEwOSA2NC4zMzU5VjU4LjEwNzRaTTE0Mi43ODcgNjYuMTE3MkMxNDIuMzE4IDY2LjExNzIgMTQxLjg5NSA2Ni4wNDEgMTQxLjUxNiA2NS44ODg3QzE0MS4xNDEgNjUuNzMyNCAxNDAuODIgNjUuNTE1NiAxNDAuNTU1IDY1LjIzODNDMTQwLjI5MyA2NC45NjA5IDE0MC4wOTIgNjQuNjM0OCAxMzkuOTUxIDY0LjI1OThDMTM5LjgxMSA2My44ODQ4IDEzOS43NCA2My40ODA1IDEzOS43NCA2My4wNDY5VjYyLjgxMjVDMTM5Ljc0IDYyLjMxNjQgMTM5LjgxMiA2MS44NjcyIDEzOS45NTcgNjEuNDY0OEMxNDAuMTAyIDYxLjA2MjUgMTQwLjMwMyA2MC43MTg4IDE0MC41NjEgNjAuNDMzNkMxNDAuODE4IDYwLjE0NDUgMTQxLjEyMyA1OS45MjM4IDE0MS40NzUgNTkuNzcxNUMxNDEuODI2IDU5LjYxOTEgMTQyLjIwNyA1OS41NDMgMTQyLjYxNyA1OS41NDNDMTQzLjA3IDU5LjU0MyAxNDMuNDY3IDU5LjYxOTEgMTQzLjgwNyA1OS43NzE1QzE0NC4xNDYgNTkuOTIzOCAxNDQuNDI4IDYwLjEzODcgMTQ0LjY1IDYwLjQxNkMxNDQuODc3IDYwLjY4OTUgMTQ1LjA0NSA2MS4wMTU2IDE0NS4xNTQgNjEuMzk0NUMxNDUuMjY4IDYxLjc3MzQgMTQ1LjMyNCA2Mi4xOTE0IDE0NS4zMjQgNjIuNjQ4NFY2My4yNTJIMTQwLjQyNlY2Mi4yMzgzSDE0My45M1Y2Mi4xMjdDMTQzLjkyMiA2MS44NzMgMTQzLjg3MSA2MS42MzQ4IDE0My43NzcgNjEuNDEyMUMxNDMuNjg4IDYxLjE4OTUgMTQzLjU0OSA2MS4wMDk4IDE0My4zNjEgNjAuODczQzE0My4xNzQgNjAuNzM2MyAxNDIuOTI0IDYwLjY2OCAxNDIuNjExIDYwLjY2OEMxNDIuMzc3IDYwLjY2OCAxNDIuMTY4IDYwLjcxODggMTQxLjk4NCA2MC44MjAzQzE0MS44MDUgNjAuOTE4IDE0MS42NTQgNjEuMDYwNSAxNDEuNTMzIDYxLjI0OEMxNDEuNDEyIDYxLjQzNTUgMTQxLjMxOCA2MS42NjIxIDE0MS4yNTIgNjEuOTI3N0MxNDEuMTg5IDYyLjE4OTUgMTQxLjE1OCA2Mi40ODQ0IDE0MS4xNTggNjIuODEyNVY2My4wNDY5QzE0MS4xNTggNjMuMzI0MiAxNDEuMTk1IDYzLjU4MiAxNDEuMjcgNjMuODIwM0MxNDEuMzQ4IDY0LjA1NDcgMTQxLjQ2MSA2NC4yNTk4IDE0MS42MDkgNjQuNDM1NUMxNDEuNzU4IDY0LjYxMTMgMTQxLjkzOCA2NC43NSAxNDIuMTQ4IDY0Ljg1MTZDMTQyLjM1OSA2NC45NDkyIDE0Mi42IDY0Ljk5OCAxNDIuODY5IDY0Ljk5OEMxNDMuMjA5IDY0Ljk5OCAxNDMuNTEyIDY0LjkyOTcgMTQzLjc3NyA2NC43OTNDMTQ0LjA0MyA2NC42NTYyIDE0NC4yNzMgNjQuNDYyOSAxNDQuNDY5IDY0LjIxMjlMMTQ1LjIxMyA2NC45MzM2QzE0NS4wNzYgNjUuMTMyOCAxNDQuODk4IDY1LjMyNDIgMTQ0LjY4IDY1LjUwNzhDMTQ0LjQ2MSA2NS42ODc1IDE0NC4xOTMgNjUuODM0IDE0My44NzcgNjUuOTQ3M0MxNDMuNTY0IDY2LjA2MDUgMTQzLjIwMSA2Ni4xMTcyIDE0Mi43ODcgNjYuMTE3MlpNMTUzLjY4OCA1Ny40Mzk1VjY2SDE1Mi4yNzVWNTkuMTE1MkwxNTAuMTg0IDU5LjgyNDJWNTguNjU4MkwxNTMuNTE4IDU3LjQzOTVIMTUzLjY4OFpNMTYwLjg1MiA2NC42ODc1VjU3SDE2Mi4yN1Y2NkgxNjAuOTg2TDE2MC44NTIgNjQuNjg3NVpNMTU2LjcyNyA2Mi45MDA0VjYyLjc3NzNDMTU2LjcyNyA2Mi4yOTY5IDE1Ni43ODMgNjEuODU5NCAxNTYuODk2IDYxLjQ2NDhDMTU3LjAxIDYxLjA2NjQgMTU3LjE3NCA2MC43MjQ2IDE1Ny4zODkgNjAuNDM5NUMxNTcuNjA0IDYwLjE1MDQgMTU3Ljg2NSA1OS45Mjk3IDE1OC4xNzQgNTkuNzc3M0MxNTguNDgyIDU5LjYyMTEgMTU4LjgzIDU5LjU0MyAxNTkuMjE3IDU5LjU0M0MxNTkuNiA1OS41NDMgMTU5LjkzNiA1OS42MTcyIDE2MC4yMjUgNTkuNzY1NkMxNjAuNTE0IDU5LjkxNDEgMTYwLjc2IDYwLjEyNyAxNjAuOTYzIDYwLjQwNDNDMTYxLjE2NiA2MC42Nzc3IDE2MS4zMjggNjEuMDA1OSAxNjEuNDQ5IDYxLjM4ODdDMTYxLjU3IDYxLjc2NzYgMTYxLjY1NiA2Mi4xODk1IDE2MS43MDcgNjIuNjU0M1Y2My4wNDY5QzE2MS42NTYgNjMuNSAxNjEuNTcgNjMuOTE0MSAxNjEuNDQ5IDY0LjI4OTFDMTYxLjMyOCA2NC42NjQxIDE2MS4xNjYgNjQuOTg4MyAxNjAuOTYzIDY1LjI2MTdDMTYwLjc2IDY1LjUzNTIgMTYwLjUxMiA2NS43NDYxIDE2MC4yMTkgNjUuODk0NUMxNTkuOTMgNjYuMDQzIDE1OS41OTIgNjYuMTE3MiAxNTkuMjA1IDY2LjExNzJDMTU4LjgyMiA2Ni4xMTcyIDE1OC40NzcgNjYuMDM3MSAxNTguMTY4IDY1Ljg3N0MxNTcuODYzIDY1LjcxNjggMTU3LjYwNCA2NS40OTIyIDE1Ny4zODkgNjUuMjAzMUMxNTcuMTc0IDY0LjkxNDEgMTU3LjAxIDY0LjU3NDIgMTU2Ljg5NiA2NC4xODM2QzE1Ni43ODMgNjMuNzg5MSAxNTYuNzI3IDYzLjM2MTMgMTU2LjcyNyA2Mi45MDA0Wk0xNTguMTM5IDYyLjc3NzNWNjIuOTAwNEMxNTguMTM5IDYzLjE4OTUgMTU4LjE2NCA2My40NTkgMTU4LjIxNSA2My43MDlDMTU4LjI3IDYzLjk1OSAxNTguMzU0IDY0LjE3OTcgMTU4LjQ2NyA2NC4zNzExQzE1OC41OCA2NC41NTg2IDE1OC43MjcgNjQuNzA3IDE1OC45MDYgNjQuODE2NEMxNTkuMDkgNjQuOTIxOSAxNTkuMzA5IDY0Ljk3NDYgMTU5LjU2MiA2NC45NzQ2QzE1OS44ODMgNjQuOTc0NiAxNjAuMTQ2IDY0LjkwNDMgMTYwLjM1NCA2NC43NjM3QzE2MC41NjEgNjQuNjIzIDE2MC43MjMgNjQuNDMzNiAxNjAuODQgNjQuMTk1M0MxNjAuOTYxIDYzLjk1MzEgMTYxLjA0MyA2My42ODM2IDE2MS4wODYgNjMuMzg2N1Y2Mi4zMjYyQzE2MS4wNjIgNjIuMDk1NyAxNjEuMDE0IDYxLjg4MDkgMTYwLjkzOSA2MS42ODE2QzE2MC44NjkgNjEuNDgyNCAxNjAuNzczIDYxLjMwODYgMTYwLjY1MiA2MS4xNjAyQzE2MC41MzEgNjEuMDA3OCAxNjAuMzgxIDYwLjg5MDYgMTYwLjIwMSA2MC44MDg2QzE2MC4wMjUgNjAuNzIyNyAxNTkuODE2IDYwLjY3OTcgMTU5LjU3NCA2MC42Nzk3QzE1OS4zMTYgNjAuNjc5NyAxNTkuMDk4IDYwLjczNDQgMTU4LjkxOCA2MC44NDM4QzE1OC43MzggNjAuOTUzMSAxNTguNTkgNjEuMTAzNSAxNTguNDczIDYxLjI5NDlDMTU4LjM1OSA2MS40ODYzIDE1OC4yNzUgNjEuNzA5IDE1OC4yMjEgNjEuOTYyOUMxNTguMTY2IDYyLjIxNjggMTU4LjEzOSA2Mi40ODgzIDE1OC4xMzkgNjIuNzc3M1pNMTcwLjgwOSA2NC43Mjg1VjYxLjcwNTFDMTcwLjgwOSA2MS40Nzg1IDE3MC43NjggNjEuMjgzMiAxNzAuNjg2IDYxLjExOTFDMTcwLjYwNCA2MC45NTUxIDE3MC40NzkgNjAuODI4MSAxNzAuMzExIDYwLjczODNDMTcwLjE0NiA2MC42NDg0IDE2OS45MzkgNjAuNjAzNSAxNjkuNjg5IDYwLjYwMzVDMTY5LjQ1OSA2MC42MDM1IDE2OS4yNiA2MC42NDI2IDE2OS4wOTIgNjAuNzIwN0MxNjguOTI0IDYwLjc5ODggMTY4Ljc5MyA2MC45MDQzIDE2OC42OTkgNjEuMDM3MUMxNjguNjA1IDYxLjE2OTkgMTY4LjU1OSA2MS4zMjAzIDE2OC41NTkgNjEuNDg4M0gxNjcuMTUyQzE2Ny4xNTIgNjEuMjM4MyAxNjcuMjEzIDYwLjk5NjEgMTY3LjMzNCA2MC43NjE3QzE2Ny40NTUgNjAuNTI3MyAxNjcuNjMxIDYwLjMxODQgMTY3Ljg2MSA2MC4xMzQ4QzE2OC4wOTIgNTkuOTUxMiAxNjguMzY3IDU5LjgwNjYgMTY4LjY4OCA1OS43MDEyQzE2OS4wMDggNTkuNTk1NyAxNjkuMzY3IDU5LjU0MyAxNjkuNzY2IDU5LjU0M0MxNzAuMjQyIDU5LjU0MyAxNzAuNjY0IDU5LjYyMyAxNzEuMDMxIDU5Ljc4MzJDMTcxLjQwMiA1OS45NDM0IDE3MS42OTMgNjAuMTg1NSAxNzEuOTA0IDYwLjUwOThDMTcyLjExOSA2MC44MzAxIDE3Mi4yMjcgNjEuMjMyNCAxNzIuMjI3IDYxLjcxNjhWNjQuNTM1MkMxNzIuMjI3IDY0LjgyNDIgMTcyLjI0NiA2NS4wODQgMTcyLjI4NSA2NS4zMTQ1QzE3Mi4zMjggNjUuNTQxIDE3Mi4zODkgNjUuNzM4MyAxNzIuNDY3IDY1LjkwNjJWNjZIMTcxLjAyQzE3MC45NTMgNjUuODQ3NyAxNzAuOSA2NS42NTQzIDE3MC44NjEgNjUuNDE5OUMxNzAuODI2IDY1LjE4MTYgMTcwLjgwOSA2NC45NTEyIDE3MC44MDkgNjQuNzI4NVpNMTcxLjAxNCA2Mi4xNDQ1TDE3MS4wMjUgNjMuMDE3NkgxNzAuMDEyQzE2OS43NSA2My4wMTc2IDE2OS41MiA2My4wNDMgMTY5LjMyIDYzLjA5MzhDMTY5LjEyMSA2My4xNDA2IDE2OC45NTUgNjMuMjEwOSAxNjguODIyIDYzLjMwNDdDMTY4LjY4OSA2My4zOTg0IDE2OC41OSA2My41MTE3IDE2OC41MjMgNjMuNjQ0NUMxNjguNDU3IDYzLjc3NzMgMTY4LjQyNCA2My45Mjc3IDE2OC40MjQgNjQuMDk1N0MxNjguNDI0IDY0LjI2MzcgMTY4LjQ2MyA2NC40MTggMTY4LjU0MSA2NC41NTg2QzE2OC42MTkgNjQuNjk1MyAxNjguNzMyIDY0LjgwMjcgMTY4Ljg4MSA2NC44ODA5QzE2OS4wMzMgNjQuOTU5IDE2OS4yMTcgNjQuOTk4IDE2OS40MzIgNjQuOTk4QzE2OS43MjEgNjQuOTk4IDE2OS45NzMgNjQuOTM5NSAxNzAuMTg4IDY0LjgyMjNDMTcwLjQwNiA2NC43MDEyIDE3MC41NzggNjQuNTU0NyAxNzAuNzAzIDY0LjM4MjhDMTcwLjgyOCA2NC4yMDcgMTcwLjg5NSA2NC4wNDEgMTcwLjkwMiA2My44ODQ4TDE3MS4zNTkgNjQuNTExN0MxNzEuMzEyIDY0LjY3MTkgMTcxLjIzMiA2NC44NDM4IDE3MS4xMTkgNjUuMDI3M0MxNzEuMDA2IDY1LjIxMDkgMTcwLjg1NyA2NS4zODY3IDE3MC42NzQgNjUuNTU0N0MxNzAuNDk0IDY1LjcxODggMTcwLjI3NyA2NS44NTM1IDE3MC4wMjMgNjUuOTU5QzE2OS43NzMgNjYuMDY0NSAxNjkuNDg0IDY2LjExNzIgMTY5LjE1NiA2Ni4xMTcyQzE2OC43NDIgNjYuMTE3MiAxNjguMzczIDY2LjAzNTIgMTY4LjA0OSA2NS44NzExQzE2Ny43MjUgNjUuNzAzMSAxNjcuNDcxIDY1LjQ3ODUgMTY3LjI4NyA2NS4xOTczQzE2Ny4xMDQgNjQuOTEyMSAxNjcuMDEyIDY0LjU4OTggMTY3LjAxMiA2NC4yMzA1QzE2Ny4wMTIgNjMuODk0NSAxNjcuMDc0IDYzLjU5NzcgMTY3LjE5OSA2My4zMzk4QzE2Ny4zMjggNjMuMDc4MSAxNjcuNTE2IDYyLjg1OTQgMTY3Ljc2MiA2Mi42ODM2QzE2OC4wMTIgNjIuNTA3OCAxNjguMzE2IDYyLjM3NSAxNjguNjc2IDYyLjI4NTJDMTY5LjAzNSA2Mi4xOTE0IDE2OS40NDUgNjIuMTQ0NSAxNjkuOTA2IDYyLjE0NDVIMTcxLjAxNFpNMTc4LjAxNCA1OS42NjAySDE3OS4yOTdWNjUuODI0MkMxNzkuMjk3IDY2LjM5NDUgMTc5LjE3NiA2Ni44Nzg5IDE3OC45MzQgNjcuMjc3M0MxNzguNjkxIDY3LjY3NTggMTc4LjM1NCA2Ny45Nzg1IDE3Ny45MiA2OC4xODU1QzE3Ny40ODYgNjguMzk2NSAxNzYuOTg0IDY4LjUwMiAxNzYuNDE0IDY4LjUwMkMxNzYuMTcyIDY4LjUwMiAxNzUuOTAyIDY4LjQ2NjggMTc1LjYwNSA2OC4zOTY1QzE3NS4zMTIgNjguMzI2MiAxNzUuMDI3IDY4LjIxMjkgMTc0Ljc1IDY4LjA1NjZDMTc0LjQ3NyA2Ny45MDQzIDE3NC4yNDggNjcuNzAzMSAxNzQuMDY0IDY3LjQ1MzFMMTc0LjcyNyA2Ni42MjExQzE3NC45NTMgNjYuODkwNiAxNzUuMjAzIDY3LjA4NzkgMTc1LjQ3NyA2Ny4yMTI5QzE3NS43NSA2Ny4zMzc5IDE3Ni4wMzcgNjcuNDAwNCAxNzYuMzM4IDY3LjQwMDRDMTc2LjY2MiA2Ny40MDA0IDE3Ni45MzggNjcuMzM5OCAxNzcuMTY0IDY3LjIxODhDMTc3LjM5NSA2Ny4xMDE2IDE3Ny41NzIgNjYuOTI3NyAxNzcuNjk3IDY2LjY5NzNDMTc3LjgyMiA2Ni40NjY4IDE3Ny44ODUgNjYuMTg1NSAxNzcuODg1IDY1Ljg1MzVWNjEuMDk1N0wxNzguMDE0IDU5LjY2MDJaTTE3My43MDcgNjIuOTAwNFY2Mi43NzczQzE3My43MDcgNjIuMjk2OSAxNzMuNzY2IDYxLjg1OTQgMTczLjg4MyA2MS40NjQ4QzE3NCA2MS4wNjY0IDE3NC4xNjggNjAuNzI0NiAxNzQuMzg3IDYwLjQzOTVDMTc0LjYwNSA2MC4xNTA0IDE3NC44NzEgNTkuOTI5NyAxNzUuMTg0IDU5Ljc3NzNDMTc1LjQ5NiA1OS42MjExIDE3NS44NSA1OS41NDMgMTc2LjI0NCA1OS41NDNDMTc2LjY1NCA1OS41NDMgMTc3LjAwNCA1OS42MTcyIDE3Ny4yOTMgNTkuNzY1NkMxNzcuNTg2IDU5LjkxNDEgMTc3LjgzIDYwLjEyNyAxNzguMDI1IDYwLjQwNDNDMTc4LjIyMSA2MC42Nzc3IDE3OC4zNzMgNjEuMDA1OSAxNzguNDgyIDYxLjM4ODdDMTc4LjU5NiA2MS43Njc2IDE3OC42OCA2Mi4xODk1IDE3OC43MzQgNjIuNjU0M1Y2My4wNDY5QzE3OC42ODQgNjMuNSAxNzguNTk4IDYzLjkxNDEgMTc4LjQ3NyA2NC4yODkxQzE3OC4zNTUgNjQuNjY0MSAxNzguMTk1IDY0Ljk4ODMgMTc3Ljk5NiA2NS4yNjE3QzE3Ny43OTcgNjUuNTM1MiAxNzcuNTUxIDY1Ljc0NjEgMTc3LjI1OCA2NS44OTQ1QzE3Ni45NjkgNjYuMDQzIDE3Ni42MjcgNjYuMTE3MiAxNzYuMjMyIDY2LjExNzJDMTc1Ljg0NiA2Ni4xMTcyIDE3NS40OTYgNjYuMDM3MSAxNzUuMTg0IDY1Ljg3N0MxNzQuODc1IDY1LjcxNjggMTc0LjYwOSA2NS40OTIyIDE3NC4zODcgNjUuMjAzMUMxNzQuMTY4IDY0LjkxNDEgMTc0IDY0LjU3NDIgMTczLjg4MyA2NC4xODM2QzE3My43NjYgNjMuNzg5MSAxNzMuNzA3IDYzLjM2MTMgMTczLjcwNyA2Mi45MDA0Wk0xNzUuMTE5IDYyLjc3NzNWNjIuOTAwNEMxNzUuMTE5IDYzLjE4OTUgMTc1LjE0NiA2My40NTkgMTc1LjIwMSA2My43MDlDMTc1LjI2IDYzLjk1OSAxNzUuMzQ4IDY0LjE3OTcgMTc1LjQ2NSA2NC4zNzExQzE3NS41ODYgNjQuNTU4NiAxNzUuNzM4IDY0LjcwNyAxNzUuOTIyIDY0LjgxNjRDMTc2LjEwOSA2NC45MjE5IDE3Ni4zMyA2NC45NzQ2IDE3Ni41ODQgNjQuOTc0NkMxNzYuOTE2IDY0Ljk3NDYgMTc3LjE4OCA2NC45MDQzIDE3Ny4zOTggNjQuNzYzN0MxNzcuNjEzIDY0LjYyMyAxNzcuNzc3IDY0LjQzMzYgMTc3Ljg5MSA2NC4xOTUzQzE3OC4wMDggNjMuOTUzMSAxNzguMDkgNjMuNjgzNiAxNzguMTM3IDYzLjM4NjdWNjIuMzI2MkMxNzguMTEzIDYyLjA5NTcgMTc4LjA2NCA2MS44ODA5IDE3Ny45OSA2MS42ODE2QzE3Ny45MiA2MS40ODI0IDE3Ny44MjQgNjEuMzA4NiAxNzcuNzAzIDYxLjE2MDJDMTc3LjU4MiA2MS4wMDc4IDE3Ny40MyA2MC44OTA2IDE3Ny4yNDYgNjAuODA4NkMxNzcuMDYyIDYwLjcyMjcgMTc2Ljg0NiA2MC42Nzk3IDE3Ni41OTYgNjAuNjc5N0MxNzYuMzQyIDYwLjY3OTcgMTc2LjEyMSA2MC43MzQ0IDE3NS45MzQgNjAuODQzOEMxNzUuNzQ2IDYwLjk1MzEgMTc1LjU5MiA2MS4xMDM1IDE3NS40NzEgNjEuMjk0OUMxNzUuMzU0IDYxLjQ4NjMgMTc1LjI2NiA2MS43MDkgMTc1LjIwNyA2MS45NjI5QzE3NS4xNDggNjIuMjE2OCAxNzUuMTE5IDYyLjQ4ODMgMTc1LjExOSA2Mi43NzczWk0xODAuNzQyIDYyLjkwMDRWNjIuNzY1NkMxODAuNzQyIDYyLjMwODYgMTgwLjgwOSA2MS44ODQ4IDE4MC45NDEgNjEuNDk0MUMxODEuMDc0IDYxLjA5OTYgMTgxLjI2NiA2MC43NTc4IDE4MS41MTYgNjAuNDY4OEMxODEuNzcgNjAuMTc1OCAxODIuMDc4IDU5Ljk0OTIgMTgyLjQ0MSA1OS43ODkxQzE4Mi44MDkgNTkuNjI1IDE4My4yMjMgNTkuNTQzIDE4My42ODQgNTkuNTQzQzE4NC4xNDggNTkuNTQzIDE4NC41NjIgNTkuNjI1IDE4NC45MjYgNTkuNzg5MUMxODUuMjkzIDU5Ljk0OTIgMTg1LjYwNCA2MC4xNzU4IDE4NS44NTcgNjAuNDY4OEMxODYuMTExIDYwLjc1NzggMTg2LjMwNSA2MS4wOTk2IDE4Ni40MzggNjEuNDk0MUMxODYuNTcgNjEuODg0OCAxODYuNjM3IDYyLjMwODYgMTg2LjYzNyA2Mi43NjU2VjYyLjkwMDRDMTg2LjYzNyA2My4zNTc0IDE4Ni41NyA2My43ODEyIDE4Ni40MzggNjQuMTcxOUMxODYuMzA1IDY0LjU2MjUgMTg2LjExMSA2NC45MDQzIDE4NS44NTcgNjUuMTk3M0MxODUuNjA0IDY1LjQ4NjMgMTg1LjI5NSA2NS43MTI5IDE4NC45MzIgNjUuODc3QzE4NC41NjggNjYuMDM3MSAxODQuMTU2IDY2LjExNzIgMTgzLjY5NSA2Ni4xMTcyQzE4My4yMyA2Ni4xMTcyIDE4Mi44MTQgNjYuMDM3MSAxODIuNDQ3IDY1Ljg3N0MxODIuMDg0IDY1LjcxMjkgMTgxLjc3NSA2NS40ODYzIDE4MS41MjEgNjUuMTk3M0MxODEuMjY4IDY0LjkwNDMgMTgxLjA3NCA2NC41NjI1IDE4MC45NDEgNjQuMTcxOUMxODAuODA5IDYzLjc4MTIgMTgwLjc0MiA2My4zNTc0IDE4MC43NDIgNjIuOTAwNFpNMTgyLjE1NCA2Mi43NjU2VjYyLjkwMDRDMTgyLjE1NCA2My4xODU1IDE4Mi4xODQgNjMuNDU1MSAxODIuMjQyIDYzLjcwOUMxODIuMzAxIDYzLjk2MjkgMTgyLjM5MyA2NC4xODU1IDE4Mi41MTggNjQuMzc3QzE4Mi42NDMgNjQuNTY4NCAxODIuODAzIDY0LjcxODggMTgyLjk5OCA2NC44MjgxQzE4My4xOTMgNjQuOTM3NSAxODMuNDI2IDY0Ljk5MjIgMTgzLjY5NSA2NC45OTIyQzE4My45NTcgNjQuOTkyMiAxODQuMTg0IDY0LjkzNzUgMTg0LjM3NSA2NC44MjgxQzE4NC41NyA2NC43MTg4IDE4NC43MyA2NC41Njg0IDE4NC44NTUgNjQuMzc3QzE4NC45OCA2NC4xODU1IDE4NS4wNzIgNjMuOTYyOSAxODUuMTMxIDYzLjcwOUMxODUuMTkzIDYzLjQ1NTEgMTg1LjIyNSA2My4xODU1IDE4NS4yMjUgNjIuOTAwNFY2Mi43NjU2QzE4NS4yMjUgNjIuNDg0NCAxODUuMTkzIDYyLjIxODggMTg1LjEzMSA2MS45Njg4QzE4NS4wNzIgNjEuNzE0OCAxODQuOTc5IDYxLjQ5MDIgMTg0Ljg1IDYxLjI5NDlDMTg0LjcyNSA2MS4wOTk2IDE4NC41NjQgNjAuOTQ3MyAxODQuMzY5IDYwLjgzNzlDMTg0LjE3OCA2MC43MjQ2IDE4My45NDkgNjAuNjY4IDE4My42ODQgNjAuNjY4QzE4My40MTggNjAuNjY4IDE4My4xODggNjAuNzI0NiAxODIuOTkyIDYwLjgzNzlDMTgyLjgwMSA2MC45NDczIDE4Mi42NDMgNjEuMDk5NiAxODIuNTE4IDYxLjI5NDlDMTgyLjM5MyA2MS40OTAyIDE4Mi4zMDEgNjEuNzE0OCAxODIuMjQyIDYxLjk2ODhDMTgyLjE4NCA2Mi4yMTg4IDE4Mi4xNTQgNjIuNDg0NCAxODIuMTU0IDYyLjc2NTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjM4Ii8+CjxwYXRoIGQ9Ik0yODMuNTc0IDYzLjEyNVY2OEgyNTguNzkzVjYzLjgxMDVMMjcwLjgyOCA1MC42ODM2QzI3Mi4xNDggNDkuMTk0IDI3My4xODkgNDcuOTA3NiAyNzMuOTUxIDQ2LjgyNDJDMjc0LjcxMyA0NS43NDA5IDI3NS4yNDYgNDQuNzY3NiAyNzUuNTUxIDQzLjkwNDNDMjc1Ljg3MiA0My4wMjQxIDI3Ni4wMzMgNDIuMTY5MyAyNzYuMDMzIDQxLjMzOThDMjc2LjAzMyA0MC4xNzE5IDI3NS44MTMgMzkuMTQ3OCAyNzUuMzczIDM4LjI2NzZDMjc0Ljk1IDM3LjM3MDQgMjc0LjMyNCAzNi42NjggMjczLjQ5NCAzNi4xNjAyQzI3Mi42NjUgMzUuNjM1NCAyNzEuNjU4IDM1LjM3MyAyNzAuNDczIDM1LjM3M0MyNjkuMTAyIDM1LjM3MyAyNjcuOTUxIDM1LjY2OTMgMjY3LjAyIDM2LjI2MTdDMjY2LjA4OSAzNi44NTQyIDI2NS4zODYgMzcuNjc1MSAyNjQuOTEyIDM4LjcyNDZDMjY0LjQzOCAzOS43NTcyIDI2NC4yMDEgNDAuOTQyMSAyNjQuMjAxIDQyLjI3OTNIMjU4LjA4MkMyNTguMDgyIDQwLjEyOTYgMjU4LjU3MyAzOC4xNjYgMjU5LjU1NSAzNi4zODg3QzI2MC41MzYgMzQuNTk0NCAyNjEuOTU4IDMzLjE3MjUgMjYzLjgyIDMyLjEyM0MyNjUuNjgyIDMxLjA1NjYgMjY3LjkyNSAzMC41MjM0IDI3MC41NDkgMzAuNTIzNEMyNzMuMDIgMzAuNTIzNCAyNzUuMTE5IDMwLjkzODIgMjc2Ljg0NiAzMS43Njc2QzI3OC41NzIgMzIuNTk3IDI3OS44ODQgMzMuNzczNCAyODAuNzgxIDM1LjI5NjlDMjgxLjY5NSAzNi44MjAzIDI4Mi4xNTIgMzguNjIzIDI4Mi4xNTIgNDAuNzA1MUMyODIuMTUyIDQxLjg1NjEgMjgxLjk2NiA0Mi45OTg3IDI4MS41OTQgNDQuMTMyOEMyODEuMjIxIDQ1LjI2NjkgMjgwLjY4OCA0Ni40MDEgMjc5Ljk5NCA0Ny41MzUyQzI3OS4zMTcgNDguNjUyMyAyNzguNTEzIDQ5Ljc3OCAyNzcuNTgyIDUwLjkxMjFDMjc2LjY1MSA1Mi4wMjkzIDI3NS42MjcgNTMuMTYzNCAyNzQuNTEgNTQuMzE0NUwyNjYuNTEyIDYzLjEyNUgyODMuNTc0Wk0zMTIuMjE5IDYzLjEyNVY2OEgyODcuNDM4VjYzLjgxMDVMMjk5LjQ3MyA1MC42ODM2QzMwMC43OTMgNDkuMTk0IDMwMS44MzQgNDcuOTA3NiAzMDIuNTk2IDQ2LjgyNDJDMzAzLjM1OCA0NS43NDA5IDMwMy44OTEgNDQuNzY3NiAzMDQuMTk1IDQzLjkwNDNDMzA0LjUxNyA0My4wMjQxIDMwNC42NzggNDIuMTY5MyAzMDQuNjc4IDQxLjMzOThDMzA0LjY3OCA0MC4xNzE5IDMwNC40NTggMzkuMTQ3OCAzMDQuMDE4IDM4LjI2NzZDMzAzLjU5NSAzNy4zNzA0IDMwMi45NjggMzYuNjY4IDMwMi4xMzkgMzYuMTYwMkMzMDEuMzA5IDM1LjYzNTQgMzAwLjMwMiAzNS4zNzMgMjk5LjExNyAzNS4zNzNDMjk3Ljc0NiAzNS4zNzMgMjk2LjU5NSAzNS42NjkzIDI5NS42NjQgMzYuMjYxN0MyOTQuNzMzIDM2Ljg1NDIgMjk0LjAzMSAzNy42NzUxIDI5My41NTcgMzguNzI0NkMyOTMuMDgzIDM5Ljc1NzIgMjkyLjg0NiA0MC45NDIxIDI5Mi44NDYgNDIuMjc5M0gyODYuNzI3QzI4Ni43MjcgNDAuMTI5NiAyODcuMjE4IDM4LjE2NiAyODguMTk5IDM2LjM4ODdDMjg5LjE4MSAzNC41OTQ0IDI5MC42MDMgMzMuMTcyNSAyOTIuNDY1IDMyLjEyM0MyOTQuMzI3IDMxLjA1NjYgMjk2LjU3IDMwLjUyMzQgMjk5LjE5NCAzMC41MjM0QzMwMS42NjUgMzAuNTIzNCAzMDMuNzY0IDMwLjkzODIgMzA1LjQ5IDMxLjc2NzZDMzA3LjIxNyAzMi41OTcgMzA4LjUyOSAzMy43NzM0IDMwOS40MjYgMzUuMjk2OUMzMTAuMzQgMzYuODIwMyAzMTAuNzk3IDM4LjYyMyAzMTAuNzk3IDQwLjcwNTFDMzEwLjc5NyA0MS44NTYxIDMxMC42MTEgNDIuOTk4NyAzMTAuMjM4IDQ0LjEzMjhDMzA5Ljg2NiA0NS4yNjY5IDMwOS4zMzMgNDYuNDAxIDMwOC42MzkgNDcuNTM1MkMzMDcuOTYyIDQ4LjY1MjMgMzA3LjE1OCA0OS43NzggMzA2LjIyNyA1MC45MTIxQzMwNS4yOTYgNTIuMDI5MyAzMDQuMjcyIDUzLjE2MzQgMzAzLjE1NCA1NC4zMTQ1TDI5NS4xNTYgNjMuMTI1SDMxMi4yMTlaTTMxNi41NjUgMzcuMzAyN0MzMTYuNTY1IDM2LjA2NzEgMzE2Ljg2OSAzNC45MzI5IDMxNy40NzkgMzMuOTAwNEMzMTguMDg4IDMyLjg2NzggMzE4LjkwMSAzMi4wNDY5IDMxOS45MTYgMzEuNDM3NUMzMjAuOTQ5IDMwLjgxMTIgMzIyLjA2NiAzMC40OTggMzIzLjI2OCAzMC40OThDMzI0LjQ4NyAzMC40OTggMzI1LjU5NSAzMC44MTEyIDMyNi41OTQgMzEuNDM3NUMzMjcuNTkzIDMyLjA0NjkgMzI4LjM4OCAzMi44Njc4IDMyOC45ODEgMzMuOTAwNEMzMjkuNTkgMzQuOTMyOSAzMjkuODk1IDM2LjA2NzEgMzI5Ljg5NSAzNy4zMDI3QzMyOS44OTUgMzguNTM4NCAzMjkuNTkgMzkuNjcyNSAzMjguOTgxIDQwLjcwNTFDMzI4LjM4OCA0MS43MjA3IDMyNy41OTMgNDIuNTI0NyAzMjYuNTk0IDQzLjExNzJDMzI1LjU5NSA0My43MDk2IDMyNC40ODcgNDQuMDA1OSAzMjMuMjY4IDQ0LjAwNTlDMzIyLjA2NiA0NC4wMDU5IDMyMC45NDkgNDMuNzA5NiAzMTkuOTE2IDQzLjExNzJDMzE4LjkwMSA0Mi41MjQ3IDMxOC4wODggNDEuNzIwNyAzMTcuNDc5IDQwLjcwNTFDMzE2Ljg2OSAzOS42NzI1IDMxNi41NjUgMzguNTM4NCAzMTYuNTY1IDM3LjMwMjdaTTMxOS45OTMgMzcuMzAyN0MzMTkuOTkzIDM4LjIxNjggMzIwLjMxNCAzOC45ODcgMzIwLjk1NyAzOS42MTMzQzMyMS42MDEgNDAuMjIyNyAzMjIuMzcxIDQwLjUyNzMgMzIzLjI2OCA0MC41MjczQzMyNC4xNjUgNDAuNTI3MyAzMjQuOTE4IDQwLjIyMjcgMzI1LjUyOCAzOS42MTMzQzMyNi4xMzcgMzkuMDAzOSAzMjYuNDQyIDM4LjIzMzcgMzI2LjQ0MiAzNy4zMDI3QzMyNi40NDIgMzYuMzU0OCAzMjYuMTM3IDM1LjU2NzcgMzI1LjUyOCAzNC45NDE0QzMyNC45MTggMzQuMzE1MSAzMjQuMTY1IDM0LjAwMiAzMjMuMjY4IDM0LjAwMkMzMjIuMzcxIDM0LjAwMiAzMjEuNjAxIDM0LjMxNTEgMzIwLjk1NyAzNC45NDE0QzMyMC4zMTQgMzUuNTY3NyAzMTkuOTkzIDM2LjM1NDggMzE5Ljk5MyAzNy4zMDI3Wk0zNTcuODc5IDU1Ljk2NDhIMzY0LjIyN0MzNjQuMDI0IDU4LjM4NTQgMzYzLjM0NyA2MC41NDM2IDM2Mi4xOTYgNjIuNDM5NUMzNjEuMDQ1IDY0LjMxODQgMzU5LjQyOCA2NS43OTk1IDM1Ny4zNDYgNjYuODgyOEMzNTUuMjY0IDY3Ljk2NjEgMzUyLjczNCA2OC41MDc4IDM0OS43NTQgNjguNTA3OEMzNDcuNDY5IDY4LjUwNzggMzQ1LjQxMyA2OC4xMDE2IDM0My41ODQgNjcuMjg5MUMzNDEuNzU2IDY2LjQ1OTYgMzQwLjE5MSA2NS4yOTE3IDMzOC44ODcgNjMuNzg1MkMzMzcuNTg0IDYyLjI2MTcgMzM2LjU4NSA2MC40MjUxIDMzNS44OTEgNTguMjc1NEMzMzUuMjE0IDU2LjEyNTcgMzM0Ljg3NSA1My43MjIgMzM0Ljg3NSA1MS4wNjQ1VjQ3Ljk5MjJDMzM0Ljg3NSA0NS4zMzQ2IDMzNS4yMjIgNDIuOTMxIDMzNS45MTYgNDAuNzgxMkMzMzYuNjI3IDM4LjYzMTUgMzM3LjY0MyAzNi43OTQ5IDMzOC45NjMgMzUuMjcxNUMzNDAuMjg0IDMzLjczMTEgMzQxLjg2NiAzMi41NTQ3IDM0My43MTEgMzEuNzQyMkMzNDUuNTczIDMwLjkyOTcgMzQ3LjY2NCAzMC41MjM0IDM0OS45ODMgMzAuNTIzNEMzNTIuOTI4IDMwLjUyMzQgMzU1LjQxNiAzMS4wNjUxIDM1Ny40NDggMzIuMTQ4NEMzNTkuNDc5IDMzLjIzMTggMzYxLjA1MyAzNC43Mjk4IDM2Mi4xNyAzNi42NDI2QzM2My4zMDUgMzguNTU1MyAzNjMuOTk5IDQwLjc0NzQgMzY0LjI1MiA0My4yMTg4SDM1Ny45MDVDMzU3LjczNSA0MS42Mjc2IDM1Ny4zNjMgNDAuMjY1IDM1Ni43ODggMzkuMTMwOUMzNTYuMjI5IDM3Ljk5NjcgMzU1LjQgMzcuMTMzNSAzNTQuMjk5IDM2LjU0MUMzNTMuMTk5IDM1LjkzMTYgMzUxLjc2IDM1LjYyNyAzNDkuOTgzIDM1LjYyN0MzNDguNTI3IDM1LjYyNyAzNDcuMjU4IDM1Ljg5NzggMzQ2LjE3NCAzNi40Mzk1QzM0NS4wOTEgMzYuOTgxMSAzNDQuMTg1IDM3Ljc3NjcgMzQzLjQ1NyAzOC44MjYyQzM0Mi43MyAzOS44NzU3IDM0Mi4xOCA0MS4xNzA2IDM0MS44MDcgNDIuNzEwOUMzNDEuNDUyIDQ0LjIzNDQgMzQxLjI3NCA0NS45Nzc5IDM0MS4yNzQgNDcuOTQxNFY1MS4wNjQ1QzM0MS4yNzQgNTIuOTI2NCAzNDEuNDM1IDU0LjYxOTEgMzQxLjc1NiA1Ni4xNDI2QzM0Mi4wOTUgNTcuNjQ5MSAzNDIuNjAzIDU4Ljk0NCAzNDMuMjggNjAuMDI3M0MzNDMuOTc0IDYxLjExMDcgMzQ0Ljg1NCA2MS45NDg2IDM0NS45MiA2Mi41NDFDMzQ2Ljk4NyA2My4xMzM1IDM0OC4yNjUgNjMuNDI5NyAzNDkuNzU0IDYzLjQyOTdDMzUxLjU2NiA2My40Mjk3IDM1My4wMyA2My4xNDE5IDM1NC4xNDcgNjIuNTY2NEMzNTUuMjgxIDYxLjk5MDkgMzU2LjEzNiA2MS4xNTMgMzU2LjcxMSA2MC4wNTI3QzM1Ny4zMDQgNTguOTM1NSAzNTcuNjkzIDU3LjU3MjkgMzU3Ljg3OSA1NS45NjQ4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfMTI0Nl80NDQ0NyIgeD0iMCIgeT0iMCIgd2lkdGg9IjM5OSIgaGVpZ2h0PSIxMDgiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldCBkeT0iNCIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+CjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wNCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93XzEyNDZfNDQ0NDciLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfMTI0Nl80NDQ0NyIgcmVzdWx0PSJzaGFwZSIvPgo8L2ZpbHRlcj4KPC9kZWZzPgo8L3N2Zz4K", "description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.", "descriptor": { "type": "latest", @@ -259,10 +259,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '130px'\n };\n};\n\nself.onDestroy = function() {\n};\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '130px',\n absoluteHeader: true\n };\n};\n\nself.onDestroy = function() {\n};\n", "settingsSchema": "", "dataKeySettingsSchema": "", - "settingsDirective": "", + "settingsDirective": "tb-value-card-widget-settings", "hasBasicMode": true, "basicModeDirective": "tb-value-card-basic-config", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" diff --git a/application/src/main/data/upgrade/3.5.1/schema_update.sql b/application/src/main/data/upgrade/3.5.1/schema_update.sql index 1655ecb978..281a7fcbfa 100644 --- a/application/src/main/data/upgrade/3.5.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.5.1/schema_update.sql @@ -122,3 +122,5 @@ ALTER TABLE resource UPDATE resource SET etag = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null; +ALTER TABLE notification_request ALTER COLUMN info SET DATA TYPE varchar(1000000); + diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 30d909cd5a..f5a0cedf08 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.actors.ruleChain; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.channel.EventLoopGroup; @@ -35,7 +34,6 @@ import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.rule.engine.util.TenantIdLoader; @@ -43,7 +41,6 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; @@ -62,6 +59,8 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; @@ -115,6 +114,10 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; + /** * Created by ashvayka on 19.03.18. */ @@ -133,7 +136,7 @@ class DefaultTbContext implements TbContext { @Override public void tellSuccess(TbMsg msg) { - tellNext(msg, Collections.singleton(TbRelationTypes.SUCCESS), null); + tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS), null); } @Override @@ -212,13 +215,13 @@ class DefaultTbContext implements TbContext { @Override public void enqueueForTellFailure(TbMsg tbMsg, String failureMessage) { TopicPartitionInfo tpi = resolvePartition(tbMsg); - enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null); + enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), failureMessage, null, null); } @Override public void enqueueForTellFailure(TbMsg tbMsg, Throwable th) { TopicPartitionInfo tpi = resolvePartition(tbMsg); - enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), getFailureMessage(th), null, null); + enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbNodeConnectionType.FAILURE), getFailureMessage(th), null, null); } @Override @@ -316,11 +319,11 @@ class DefaultTbContext implements TbContext { @Override public void tellFailure(TbMsg msg, Throwable th) { if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbRelationTypes.FAILURE, th); + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th); } String failureMessage = getFailureMessage(th); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), - nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), + nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE), msg, failureMessage)); } @@ -343,67 +346,104 @@ class DefaultTbContext implements TbContext { return TbMsg.transformMsg(origMsg, type, originator, metaData, data); } + @Override + public TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(queueName, type, originator, null, metaData, data); + } + + @Override + public TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return TbMsg.newMsg(queueName, type, originator, customerId, metaData, data, nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); + } + + @Override + public TbMsg transformMsg(TbMsg origMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return TbMsg.transformMsg(origMsg, type, originator, metaData, data); + } + + @Override + public TbMsg transformMsg(TbMsg origMsg, TbMsgMetaData metaData, String data) { + return TbMsg.transformMsg(origMsg, metaData, data); + } + + @Override + public TbMsg transformMsgOriginator(TbMsg origMsg, EntityId originator) { + return TbMsg.transformMsgOriginator(origMsg, originator); + } + + @Override public TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId) { - return entityActionMsg(customer, customer.getId(), ruleNodeId, DataConstants.ENTITY_CREATED); + return entityActionMsg(customer, customer.getId(), ruleNodeId, ENTITY_CREATED); } + @Override public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) { DeviceProfile deviceProfile = null; if (device.getDeviceProfileId() != null) { deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId()); } - return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, deviceProfile); + return entityActionMsg(device, device.getId(), ruleNodeId, ENTITY_CREATED, deviceProfile); } + @Override public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) { AssetProfile assetProfile = null; if (asset.getAssetProfileId() != null) { assetProfile = mainCtx.getAssetProfileCache().find(asset.getAssetProfileId()); } - return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, assetProfile); + return entityActionMsg(asset, asset.getId(), ruleNodeId, ENTITY_CREATED, assetProfile); } + @Override public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) { + EntityId originator = alarm.getOriginator(); + HasRuleEngineProfile profile = getRuleEngineProfile(originator); + return entityActionMsg(alarm, originator, ruleNodeId, action, profile); + } + + @Override + public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, TbMsgType actionMsgType) { + EntityId originator = alarm.getOriginator(); + HasRuleEngineProfile profile = getRuleEngineProfile(originator); + return entityActionMsg(alarm, originator, ruleNodeId, actionMsgType, profile); + } + + private HasRuleEngineProfile getRuleEngineProfile(EntityId originator) { HasRuleEngineProfile profile = null; - if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) { - DeviceId deviceId = new DeviceId(alarm.getOriginator().getId()); + if (EntityType.DEVICE.equals(originator.getEntityType())) { + DeviceId deviceId = new DeviceId(originator.getId()); profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); - } else if (EntityType.ASSET.equals(alarm.getOriginator().getEntityType())) { - AssetId assetId = new AssetId(alarm.getOriginator().getId()); + } else if (EntityType.ASSET.equals(originator.getEntityType())) { + AssetId assetId = new AssetId(originator.getId()); profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId); } - return entityActionMsg(alarm, alarm.getOriginator(), ruleNodeId, action, profile); + return profile; } + @Override public TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List attributes) { ObjectNode entityNode = JacksonUtil.newObjectNode(); if (attributes != null) { attributes.forEach(attributeKvEntry -> JacksonUtil.addKvEntry(entityNode, attributeKvEntry)); } - return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_UPDATED, JacksonUtil.toString(entityNode)); + return attributesActionMsg(originator, ruleNodeId, scope, ATTRIBUTES_UPDATED, JacksonUtil.toString(entityNode)); } + @Override public TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List keys) { ObjectNode entityNode = JacksonUtil.newObjectNode(); ArrayNode attrsArrayNode = entityNode.putArray("attributes"); if (keys != null) { keys.forEach(attrsArrayNode::add); } - return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_DELETED, JacksonUtil.toString(entityNode)); + return attributesActionMsg(originator, ruleNodeId, scope, ATTRIBUTES_DELETED, JacksonUtil.toString(entityNode)); } - private TbMsg attributesActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, String action, String msgData) { + private TbMsg attributesActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, TbMsgType actionMsgType, String msgData) { TbMsgMetaData tbMsgMetaData = getActionMetaData(ruleNodeId); tbMsgMetaData.putValue("scope", scope); - HasRuleEngineProfile profile = null; - if (EntityType.DEVICE.equals(originator.getEntityType())) { - DeviceId deviceId = new DeviceId(originator.getId()); - profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); - } else if (EntityType.ASSET.equals(originator.getEntityType())) { - AssetId assetId = new AssetId(originator.getId()); - profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId); - } - return entityActionMsg(originator, tbMsgMetaData, msgData, action, profile); + HasRuleEngineProfile profile = getRuleEngineProfile(originator); + return entityActionMsg(originator, tbMsgMetaData, msgData, actionMsgType, profile); } @Override @@ -411,10 +451,11 @@ class DefaultTbContext implements TbContext { mainCtx.getClusterService().onEdgeEventUpdate(tenantId, edgeId); } - public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { - return entityActionMsg(entity, id, ruleNodeId, action, null); + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, TbMsgType actionMsgType) { + return entityActionMsg(entity, id, ruleNodeId, actionMsgType, null); } + @Deprecated(since = "3.5.2", forRemoval = true) public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, K profile) { try { return entityActionMsg(id, getActionMetaData(ruleNodeId), JacksonUtil.toString(JacksonUtil.valueToTree(entity)), action, profile); @@ -423,6 +464,7 @@ class DefaultTbContext implements TbContext { } } + @Deprecated(since = "3.5.2", forRemoval = true) private TbMsg entityActionMsg(I id, TbMsgMetaData msgMetaData, String msgData, String action, K profile) { String defaultQueueName = null; RuleChainId defaultRuleChainId = null; @@ -433,6 +475,24 @@ class DefaultTbContext implements TbContext { return TbMsg.newMsg(defaultQueueName, action, id, msgMetaData, msgData, defaultRuleChainId, null); } + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, TbMsgType actionMsgType, K profile) { + try { + return entityActionMsg(id, getActionMetaData(ruleNodeId), JacksonUtil.toString(JacksonUtil.valueToTree(entity)), actionMsgType, profile); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + actionMsgType.name() + " msg: " + e); + } + } + + private TbMsg entityActionMsg(I id, TbMsgMetaData msgMetaData, String msgData, TbMsgType actionMsgType, K profile) { + String defaultQueueName = null; + RuleChainId defaultRuleChainId = null; + if (profile != null) { + defaultQueueName = profile.getDefaultQueueName(); + defaultRuleChainId = profile.getDefaultRuleChainId(); + } + return TbMsg.newMsg(defaultQueueName, actionMsgType, id, msgMetaData, msgData, defaultRuleChainId, null); + } + @Override public RuleNodeId getSelfId() { return nodeCtx.getSelf().getId(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 4bd082ee38..3c8ddb2a7b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -16,7 +16,7 @@ package org.thingsboard.server.actors.ruleChain; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorRef; @@ -307,7 +307,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, - PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT)); + PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT, + DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); JwtTokenAuthenticationProcessingFilter filter = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher); @@ -228,6 +230,7 @@ public class ThingsboardSecurityConfiguration { .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point .antMatchers(MAIL_OAUTH2_PROCESSING_ENTRY_POINT).permitAll() // Mail oauth2 code processing url + .antMatchers(DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT).permitAll() // Mail oauth2 code processing url .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points .and() .authorizeRequests() diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index a6a49f6b3c..f31cebd258 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -24,6 +24,7 @@ public class ControllerConstants { protected static final String CUSTOMER_ID = "customerId"; protected static final String TENANT_ID = "tenantId"; protected static final String DEVICE_ID = "deviceId"; + protected static final String PROTOCOL = "protocol"; protected static final String EDGE_ID = "edgeId"; protected static final String RPC_ID = "rpcId"; protected static final String ENTITY_ID = "entityId"; @@ -34,6 +35,7 @@ public class ControllerConstants { protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the dashboard id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String PROTOCOL_PARAM_DESCRIPTION = "A string value representing the device connectivity protocol. Possible values: 'mqtt', 'mqtts', 'http', 'https', 'coap', 'coaps'"; protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java new file mode 100644 index 0000000000..04b1b4c522 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.dao.device.DeviceConnectivityService; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.system.SystemSecurityService; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.URISyntaxException; + +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL; +import static org.thingsboard.server.controller.ControllerConstants.PROTOCOL_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +@RequiredArgsConstructor +@Slf4j +public class DeviceConnectivityController extends BaseController { + + private final DeviceConnectivityService deviceConnectivityService; + private final SystemSecurityService systemSecurityService; + + @ApiOperation(value = "Get commands to publish device telemetry (getDevicePublishTelemetryCommands)", + notes = "Fetch the list of commands to publish device telemetry based on device profile " + + "If the user has the authority of 'Tenant Administrator', the server checks that the device is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the device is assigned to the same customer. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK", + examples = @io.swagger.annotations.Example( + value = { + @io.swagger.annotations.ExampleProperty( + mediaType = "application/json", + value = "{\"http\":\"curl -v -X POST http://localhost:8080/api/v1/0ySs4FTOn5WU15XLmal8/telemetry --header Content-Type:application/json --data {temperature:25}\"," + + "\"mqtt\":\"mosquitto_pub -d -q 1 -h localhost -t v1/devices/me/telemetry -i myClient1 -u myUsername1 -P myPassword -m {temperature:25}\"," + + "\"coap\":\"coap-client -m POST coap://localhost:5683/api/v1/0ySs4FTOn5WU15XLmal8/telemetry -t json -e {temperature:25}\"}")}))}) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/device-connectivity/{deviceId}", method = RequestMethod.GET) + @ResponseBody + public JsonNode getDevicePublishTelemetryCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException { + checkParameter(DEVICE_ID, strDeviceId); + DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); + Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS); + + String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); + return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device); + } + + @ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.") + @RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET) + @ResponseBody + public ResponseEntity downloadServerCertificate(@ApiParam(value = PROTOCOL_PARAM_DESCRIPTION) + @PathVariable(PROTOCOL) String protocol) throws ThingsboardException, IOException { + checkParameter(PROTOCOL, protocol); + var pemCert = + checkNotNull(deviceConnectivityService.getPemCertFile(protocol), protocol + " pem cert file is not found!"); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + PEM_CERT_FILE_NAME) + .header("x-filename", PEM_CERT_FILE_NAME) + .contentLength(pemCert.contentLength()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(pemCert); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java index 08448f1d28..e1e89734b9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -117,8 +117,8 @@ public class EntityRelationController extends BaseController { tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser()); } - @ApiOperation(value = "Delete Relations (deleteRelations)", - notes = "Deletes all the relation (both 'from' and 'to' direction) for the specified entity. " + + @ApiOperation(value = "Delete common relations (deleteCommonRelations)", + notes = "Deletes all the relations ('from' and 'to' direction) for the specified entity and relation type group: 'COMMON'. " + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"}) @@ -129,7 +129,7 @@ public class EntityRelationController extends BaseController { checkParameter("entityType", strType); EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId); checkEntityId(entityId, Operation.WRITE); - tbEntityRelationService.deleteRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser()); + tbEntityRelationService.deleteCommonRelations(getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser()); } @ApiOperation(value = "Get Relation (getRelation)", diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 464dc528a5..451a5b7dc0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rpc.Rpc; @@ -52,7 +53,6 @@ import org.thingsboard.server.service.security.permission.Operation; import javax.annotation.Nullable; import java.util.UUID; -import static org.thingsboard.server.common.data.DataConstants.RPC_DELETED; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID; import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; @@ -239,7 +239,7 @@ public class RpcV2Controller extends AbstractRpcController { rpcService.deleteRpc(getTenantId(), rpcId); rpc.setStatus(RpcStatus.DELETED); - TbMsg msg = TbMsg.newMsg(RPC_DELETED, rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); + TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_DELETED, rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); tbClusterService.pushMsgToRuleEngine(getTenantId(), rpc.getDeviceId(), msg, null); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 91583136f6..8a68dc21cb 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -221,12 +221,12 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke .build(); } - private class SessionMetaData implements SendHandler { + class SessionMetaData implements SendHandler { private final WebSocketSession session; private final RemoteEndpoint.Async asyncRemote; private final WebSocketSessionRef sessionRef; - private final AtomicBoolean isSending = new AtomicBoolean(false); + final AtomicBoolean isSending = new AtomicBoolean(false); private final Queue> msgQueue; private volatile long lastActivityTime; @@ -241,7 +241,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke this.lastActivityTime = System.currentTimeMillis(); } - synchronized void sendPing(long currentTime) { + void sendPing(long currentTime) { try { long timeSinceLastActivity = currentTime - lastActivityTime; if (timeSinceLastActivity >= pingTimeout) { @@ -256,37 +256,38 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke } } - private void closeSession(CloseStatus reason) { + void closeSession(CloseStatus reason) { try { close(this.sessionRef, reason); } catch (IOException ioe) { log.trace("[{}] Session transport error", session.getId(), ioe); + } finally { + msgQueue.clear(); } } - synchronized void processPongMessage(long currentTime) { + void processPongMessage(long currentTime) { lastActivityTime = currentTime; } - synchronized void sendMsg(String msg) { + void sendMsg(String msg) { sendMsg(new TbWebSocketTextMsg(msg)); } - synchronized void sendMsg(TbWebSocketMsg msg) { - if (isSending.compareAndSet(false, true)) { - sendMsgInternal(msg); - } else { - try { - msgQueue.add(msg); - } catch (RuntimeException e) { - if (log.isTraceEnabled()) { - log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e); - } else { - log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId()); - } - closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!")); + void sendMsg(TbWebSocketMsg msg) { + try { + msgQueue.add(msg); + } catch (RuntimeException e) { + if (log.isTraceEnabled()) { + log.trace("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId(), e); + } else { + log.info("[{}][{}] Session closed due to queue error", sessionRef.getSecurityCtx().getTenantId(), session.getId()); } + closeSession(CloseStatus.POLICY_VIOLATION.withReason("Max pending updates limit reached!")); + return; } + + processNextMsg(); } private void sendMsgInternal(TbWebSocketMsg msg) { @@ -294,9 +295,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (TbWebSocketMsgType.TEXT.equals(msg.getType())) { TbWebSocketTextMsg textMsg = (TbWebSocketTextMsg) msg; this.asyncRemote.sendText(textMsg.getMsg(), this); + // isSending status will be reset in the onResult method by call back } else { TbWebSocketPingMsg pingMsg = (TbWebSocketPingMsg) msg; - this.asyncRemote.sendPing(pingMsg.getMsg()); + this.asyncRemote.sendPing(pingMsg.getMsg()); // blocking call + isSending.set(false); processNextMsg(); } } catch (Exception e) { @@ -310,12 +313,17 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (!result.isOK()) { log.trace("[{}] Failed to send msg", session.getId(), result.getException()); closeSession(CloseStatus.SESSION_NOT_RELIABLE); - } else { - processNextMsg(); + return; } + + isSending.set(false); + processNextMsg(); } private void processNextMsg() { + if (msgQueue.isEmpty() || !isSending.compareAndSet(false, true)) { + return; + } TbWebSocketMsg msg = msgQueue.poll(); if (msg != null) { sendMsgInternal(msg); @@ -392,19 +400,21 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (tenantProfileConfiguration == null) { return true; } - + boolean limitAllowed; String sessionId = session.getId(); if (tenantProfileConfiguration.getMaxWsSessionsPerTenant() > 0) { Set tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet()); synchronized (tenantSessions) { - if (tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant()) { + limitAllowed = tenantSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerTenant(); + if (limitAllowed) { tenantSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!")); return false; - } } } @@ -412,42 +422,48 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke if (tenantProfileConfiguration.getMaxWsSessionsPerCustomer() > 0) { Set customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet()); synchronized (customerSessions) { - if (customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer()) { + limitAllowed = customerSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerCustomer(); + if (limitAllowed) { customerSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached")); return false; - } } } if (tenantProfileConfiguration.getMaxWsSessionsPerRegularUser() > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { Set regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); synchronized (regularUserSessions) { - if (regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser()) { + limitAllowed = regularUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerRegularUser(); + if (limitAllowed) { regularUserSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached")); return false; - } } } if (tenantProfileConfiguration.getMaxWsSessionsPerPublicUser() > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) { Set publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet()); synchronized (publicUserSessions) { - if (publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser()) { + limitAllowed = publicUserSessions.size() < tenantProfileConfiguration.getMaxWsSessionsPerPublicUser(); + if (limitAllowed) { publicUserSessions.add(sessionId); - } else { + } + } + if (!limitAllowed) { log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached" , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId); session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached")); return false; - } } } } diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index e068cd4d23..6c855a89c9 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -26,7 +26,6 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; @@ -38,19 +37,21 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger; +import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentTrigger; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentTrigger; -import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitTrigger; -import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionTrigger; import org.thingsboard.server.dao.audit.AuditLogService; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -63,85 +64,8 @@ public class EntityActionService { public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, TenantId tenantId, CustomerId customerId, ActionType actionType, User user, Object... additionalInfo) { - String msgType = null; - switch (actionType) { - case ADDED: - msgType = DataConstants.ENTITY_CREATED; - break; - case DELETED: - msgType = DataConstants.ENTITY_DELETED; - break; - case UPDATED: - msgType = DataConstants.ENTITY_UPDATED; - break; - case ASSIGNED_TO_CUSTOMER: - msgType = DataConstants.ENTITY_ASSIGNED; - break; - case UNASSIGNED_FROM_CUSTOMER: - msgType = DataConstants.ENTITY_UNASSIGNED; - break; - case ATTRIBUTES_UPDATED: - msgType = DataConstants.ATTRIBUTES_UPDATED; - break; - case ATTRIBUTES_DELETED: - msgType = DataConstants.ATTRIBUTES_DELETED; - break; - case ALARM_ACK: - msgType = DataConstants.ALARM_ACK; - break; - case ALARM_CLEAR: - msgType = DataConstants.ALARM_CLEAR; - break; - case ALARM_ASSIGNED: - msgType = DataConstants.ALARM_ASSIGNED; - break; - case ALARM_UNASSIGNED: - msgType = DataConstants.ALARM_UNASSIGNED; - break; - case ALARM_DELETE: - msgType = DataConstants.ALARM_DELETE; - break; - case ADDED_COMMENT: - msgType = DataConstants.COMMENT_CREATED; - break; - case UPDATED_COMMENT: - msgType = DataConstants.COMMENT_UPDATED; - break; - case ASSIGNED_FROM_TENANT: - msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT; - break; - case ASSIGNED_TO_TENANT: - msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT; - break; - case PROVISION_SUCCESS: - msgType = DataConstants.PROVISION_SUCCESS; - break; - case PROVISION_FAILURE: - msgType = DataConstants.PROVISION_FAILURE; - break; - case TIMESERIES_UPDATED: - msgType = DataConstants.TIMESERIES_UPDATED; - break; - case TIMESERIES_DELETED: - msgType = DataConstants.TIMESERIES_DELETED; - break; - case ASSIGNED_TO_EDGE: - msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE; - break; - case UNASSIGNED_FROM_EDGE: - msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE; - break; - case RELATION_ADD_OR_UPDATE: - msgType = DataConstants.RELATION_ADD_OR_UPDATE; - break; - case RELATION_DELETED: - msgType = DataConstants.RELATION_DELETED; - break; - case RELATIONS_DELETED: - msgType = DataConstants.RELATIONS_DELETED; - break; - } - if (!StringUtils.isEmpty(msgType)) { + Optional msgType = actionType.getRuleEngineMsgType(); + if (msgType.isPresent()) { try { TbMsgMetaData metaData = new TbMsgMetaData(); if (user != null) { @@ -247,7 +171,7 @@ public class EntityActionService { if (tenantId != null && !tenantId.isSysTenantId()) { processNotificationRules(tenantId, entityId, entity, actionType, user, additionalInfo); } - TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(msgType.get(), entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); } catch (Exception e) { log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e); diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index ad65ee805b..112c406d19 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -30,8 +30,12 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.api.NodeDefinition; import org.thingsboard.rule.engine.api.RuleNode; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.TbVersionedNode; +import org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode; +import org.thingsboard.rule.engine.filter.TbOriginatorTypeSwitchNode; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -194,7 +198,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode()); - NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); + NodeDefinition nodeDefinition = prepareNodeDefinition(clazz, ruleNodeAnnotation); ObjectNode configurationDescriptor = JacksonUtil.newObjectNode(); JsonNode node = JacksonUtil.valueToTree(nodeDefinition); configurationDescriptor.set("nodeDefinition", node); @@ -221,13 +225,13 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe return scannedComponent; } - private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception { + private NodeDefinition prepareNodeDefinition(Class clazz, RuleNode nodeAnnotation) throws Exception { NodeDefinition nodeDefinition = new NodeDefinition(); nodeDefinition.setDetails(nodeAnnotation.nodeDetails()); nodeDefinition.setDescription(nodeAnnotation.nodeDescription()); nodeDefinition.setInEnabled(nodeAnnotation.inEnabled()); nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled()); - nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(nodeAnnotation)); + nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(clazz, nodeAnnotation)); nodeDefinition.setCustomRelations(nodeAnnotation.customRelations()); nodeDefinition.setRuleChainNode(nodeAnnotation.ruleChainNode()); Class configClazz = nodeAnnotation.configClazz(); @@ -242,10 +246,17 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe return nodeDefinition; } - private String[] getRelationTypesWithFailureRelation(RuleNode nodeAnnotation) { + private String[] getRelationTypesWithFailureRelation(Class clazz, RuleNode nodeAnnotation) { List relationTypes = new ArrayList<>(Arrays.asList(nodeAnnotation.relationTypes())); - if (!relationTypes.contains(TbRelationTypes.FAILURE)) { - relationTypes.add(TbRelationTypes.FAILURE); + if (TbOriginatorTypeSwitchNode.class.equals(clazz)) { + relationTypes.addAll(EntityType.NORMAL_NAMES); + } + if (TbMsgTypeSwitchNode.class.equals(clazz)) { + relationTypes.addAll(TbMsgType.NODE_CONNECTIONS); + relationTypes.add(TbNodeConnectionType.OTHER); + } + if (!relationTypes.contains(TbNodeConnectionType.FAILURE)) { + relationTypes.add(TbNodeConnectionType.FAILURE); } return relationTypes.toArray(new String[relationTypes.size()]); } diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index beecebe2ba..763ce01116 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.TbMsg; @@ -162,7 +163,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { if (targetProfile.getProfileData().getProvisionConfiguration().getProvisionDeviceSecret().equals(provisionRequestSecret)) { if (targetDevice != null) { log.warn("[{}] The device is present and could not be provisioned once more!", targetDevice.getName()); - notify(targetDevice, provisionRequest, DataConstants.PROVISION_FAILURE, false); + notify(targetDevice, provisionRequest, TbMsgType.PROVISION_FAILURE, false); throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } else { return createDevice(provisionRequest, targetProfile); @@ -190,11 +191,11 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { Optional provisionState = attributesService.find(device.getTenantId(), device.getId(), DataConstants.SERVER_SCOPE, DEVICE_PROVISION_STATE).get(); if (provisionState != null && provisionState.isPresent() && !provisionState.get().getValueAsString().equals(PROVISIONED_STATE)) { - notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } else { saveProvisionStateAttribute(device).get(); - notify(device, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + notify(device, provisionRequest, TbMsgType.PROVISION_SUCCESS, true); } } catch (InterruptedException | ExecutionException e) { throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); @@ -206,7 +207,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { return processCreateDevice(provisionRequest, profile); } - private void notify(Device device, ProvisionRequest provisionRequest, String type, boolean success) { + private void notify(Device device, ProvisionRequest provisionRequest, TbMsgType type, boolean success) { pushProvisionEventToRuleEngine(provisionRequest, device, type); logAction(device.getTenantId(), device.getCustomerId(), device, success, provisionRequest); } @@ -222,14 +223,14 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { clusterService.onDeviceUpdated(savedDevice, null); saveProvisionStateAttribute(savedDevice).get(); pushDeviceCreatedEventToRuleEngine(savedDevice); - notify(savedDevice, provisionRequest, DataConstants.PROVISION_SUCCESS, true); + notify(savedDevice, provisionRequest, TbMsgType.PROVISION_SUCCESS, true); return new ProvisionResponse(getDeviceCredentials(savedDevice), ProvisionResponseStatus.SUCCESS); } catch (Exception e) { log.warn("[{}] Error during device creation from provision request: [{}]", provisionRequest.getDeviceName(), provisionRequest, e); Device device = deviceService.findDeviceByTenantIdAndName(profile.getTenantId(), provisionRequest.getDeviceName()); if (device != null) { - notify(device, provisionRequest, DataConstants.PROVISION_FAILURE, false); + notify(device, provisionRequest, TbMsgType.PROVISION_FAILURE, false); } throw new ProvisionFailedException(ProvisionResponseStatus.FAILURE.name()); } @@ -253,7 +254,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { return deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId()); } - private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, String type) { + private void pushProvisionEventToRuleEngine(ProvisionRequest request, Device device, TbMsgType type) { try { JsonNode entityNode = JacksonUtil.valueToTree(request); TbMsg msg = TbMsg.newMsg(type, device.getId(), device.getCustomerId(), createTbMsgMetaData(device), JacksonUtil.toString(entityNode)); @@ -266,10 +267,10 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { private void pushDeviceCreatedEventToRuleEngine(Device device) { try { ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); - TbMsg msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, device.getId(), device.getCustomerId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); + TbMsg msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, device.getId(), device.getCustomerId(), createTbMsgMetaData(device), JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); sendToRuleEngine(device.getTenantId(), msg, null); } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), DataConstants.ENTITY_CREATED, e); + log.warn("[{}] Failed to push device action to rule engine: {}", device.getId(), TbMsgType.ENTITY_CREATED.name(), e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index e7214177d2..d002854155 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -71,10 +72,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; -import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; - @Service @Slf4j @ConditionalOnProperty(prefix = "edges", value = "enabled", havingValue = "true") @@ -278,7 +275,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); long lastConnectTs = System.currentTimeMillis(); save(edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); - pushRuleEngineMessage(edgeGrpcSession.getEdge().getTenantId(), edgeId, lastConnectTs, CONNECT_EVENT); + pushRuleEngineMessage(edgeGrpcSession.getEdge().getTenantId(), edgeId, lastConnectTs, TbMsgType.CONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); scheduleEdgeEventsCheck(edgeGrpcSession); } @@ -398,7 +395,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i save(edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); save(edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); - pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, DISCONNECT_EVENT); + pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); cancelScheduleEdgeEventsCheck(edgeId); } else { log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); @@ -451,10 +448,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, String msgType) { + private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, TbMsgType msgType) { try { ObjectNode edgeState = JacksonUtil.newObjectNode(); - if (msgType.equals(CONNECT_EVENT)) { + if (msgType.equals(TbMsgType.CONNECT_EVENT)) { edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); } else { @@ -464,7 +461,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i String data = JacksonUtil.toString(edgeState); TbMsgMetaData md = new TbMsgMetaData(); if (!persistToTelemetry) { - md.putValue(DataConstants.SCOPE, SERVER_SCOPE); + md.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); } TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 3e617de8c6..67194cb618 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rpc.RpcError; @@ -46,7 +47,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; @@ -124,7 +124,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { try { Device device = deviceService.findDeviceById(tenantId, deviceId); ObjectNode entityNode = JacksonUtil.OBJECT_MAPPER.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, device.getCustomerId(), + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, device.getCustomerId(), getActionTbMsgMetaData(edge, device.getCustomerId()), TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { @Override @@ -138,7 +138,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { } }); } catch (JsonProcessingException | IllegalArgumentException e) { - log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, DataConstants.ENTITY_CREATED, e); + log.warn("[{}] Failed to push device action to rule engine: {}", deviceId, TbMsgType.ENTITY_CREATED.name(), e); } } @@ -216,7 +216,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor { ObjectNode data = JacksonUtil.newObjectNode(); data.put("method", deviceRpcCallMsg.getRequestMsg().getMethod()); data.put("params", deviceRpcCallMsg.getRequestMsg().getParams()); - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.TO_SERVER_RPC_REQUEST.name(), deviceId, null, metaData, + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, deviceId, null, metaData, TbMsgDataType.JSON, JacksonUtil.OBJECT_MAPPER.writeValueAsString(data)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, new TbQueueCallback() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java index fece86b92a..5942628bf3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/telemetry/BaseTelemetryProcessor.java @@ -50,11 +50,11 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.model.ModelConstants; @@ -184,7 +184,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); metaData.putValue("ts", tsKv.getTs() + ""); var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), TbMsgType.POST_TELEMETRY_REQUEST, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -228,7 +228,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -257,7 +257,7 @@ public abstract class BaseTelemetryProcessor extends BaseEdgeProcessor { @Override public void onSuccess(@Nullable Void tmp) { var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); - TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), DataConstants.ATTRIBUTES_UPDATED, entityId, + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), TbMsgType.ATTRIBUTES_UPDATED, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { @Override diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index 0a75404fb4..68b9cdecbb 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -21,7 +21,6 @@ import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; @@ -40,6 +39,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; @@ -286,7 +286,7 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS private void pushAssignedFromNotification(Tenant currentTenant, TenantId newTenantId, Device assignedDevice) { String data = JacksonUtil.toString(JacksonUtil.valueToTree(assignedDevice)); if (data != null) { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_ASSIGNED_FROM_TENANT, assignedDevice.getId(), assignedDevice.getCustomerId(), getMetaDataForAssignedFrom(currentTenant), TbMsgDataType.JSON, data); tbClusterService.pushMsgToRuleEngine(newTenantId, assignedDevice.getId(), tbMsg, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java index b978b730fd..cf1733490d 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/DefaultTbEntityRelationService.java @@ -72,9 +72,9 @@ public class DefaultTbEntityRelationService extends AbstractTbEntityService impl } @Override - public void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException { + public void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException { try { - relationService.deleteEntityRelations(tenantId, entityId); + relationService.deleteEntityCommonRelations(tenantId, entityId); notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, entityId, null, customerId, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java index 2caee86d0c..8bf5018c95 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entity/relation/TbEntityRelationService.java @@ -28,6 +28,6 @@ public interface TbEntityRelationService { void delete(TenantId tenantId, CustomerId customerId, EntityRelation entity, User user) throws ThingsboardException; - void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; + void deleteCommonRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, User user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java index 0c04e46ff5..d9f11dacb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java @@ -82,7 +82,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse UserId userId = tbUser.getId(); try { - tbAlarmService.unassignUserAlarms(tenantId, tbUser, System.currentTimeMillis()); + tbAlarmService.unassignUserAlarms(tbUser.getTenantId(), tbUser, System.currentTimeMillis()); userService.deleteUser(tenantId, userId); notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser, user, ActionType.DELETED, true, null, customerId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 7965824274..1087990c4e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -114,13 +114,14 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; + @Service @Profile("install") @Slf4j public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public static final String CUSTOMER_CRED = "customer"; - public static final String DEFAULT_DEVICE_TYPE = "default"; public static final String ACTIVITY_STATE = "active"; @Autowired diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index dafccfdad6..ff53682ca7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; @@ -502,7 +503,7 @@ public class DefaultDataUpdateService implements DataUpdateService { md.getNodes().add(ruleNode); md.setFirstNodeIndex(newIdx); - md.addConnectionInfo(newIdx, oldIdx, "Success"); + md.addConnectionInfo(newIdx, oldIdx, TbNodeConnectionType.SUCCESS); ruleChainService.saveRuleChainMetaData(tenant.getId(), md, Function.identity()); } } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 76631aaa95..63eaab23f2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -32,10 +32,10 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasRuleEngineProfile; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AssetId; @@ -178,15 +178,8 @@ public class DefaultTbClusterService implements TbClusterService { return; } } else { - if (entityId.getEntityType().equals(EntityType.DEVICE)) { - tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceId(entityId.getId()))); - } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { - tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); - } else if (entityId.getEntityType().equals(EntityType.ASSET)) { - tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetId(entityId.getId()))); - } else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) { - tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId()))); - } + HasRuleEngineProfile ruleEngineProfile = getRuleEngineProfileForEntityOrElseNull(tenantId, entityId); + tbMsg = transformMsg(tbMsg, ruleEngineProfile); } TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); @@ -198,34 +191,33 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); } - private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) { - if (deviceProfile != null) { - RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId(); - String targetQueueName = deviceProfile.getDefaultQueueName(); - tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName); - } - return tbMsg; - } - - private TbMsg transformMsg(TbMsg tbMsg, AssetProfile assetProfile) { - if (assetProfile != null) { - RuleChainId targetRuleChainId = assetProfile.getDefaultRuleChainId(); - String targetQueueName = assetProfile.getDefaultQueueName(); - tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName); + private HasRuleEngineProfile getRuleEngineProfileForEntityOrElseNull(TenantId tenantId, EntityId entityId) { + if (entityId.getEntityType().equals(EntityType.DEVICE)) { + return deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + } else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) { + return deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId())); + } else if (entityId.getEntityType().equals(EntityType.ASSET)) { + return assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + } else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) { + return assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId())); } - return tbMsg; - } - - private TbMsg transformMsg(TbMsg tbMsg, RuleChainId targetRuleChainId, String targetQueueName) { - boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); - boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); - - if (isRuleChainTransform && isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); - } else if (isRuleChainTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId); - } else if (isQueueTransform) { - tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName); + return null; + } + private TbMsg transformMsg(TbMsg tbMsg, HasRuleEngineProfile ruleEngineProfile) { + if (ruleEngineProfile != null) { + RuleChainId targetRuleChainId = ruleEngineProfile.getDefaultRuleChainId(); + String targetQueueName = ruleEngineProfile.getDefaultQueueName(); + + boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId()); + boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName()); + + if (isRuleChainTransform && isQueueTransform) { + tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName); + } else if (isRuleChainTransform) { + tbMsg = TbMsg.transformMsgRuleChainId(tbMsg, targetRuleChainId); + } else if (isQueueTransform) { + tbMsg = TbMsg.transformMsgQueueName(tbMsg, targetQueueName); + } } return tbMsg; } diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java index bca7c7d81a..8ea7208c55 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbCoreDeviceRpcService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.rpc; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +26,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -182,7 +182,7 @@ public class DefaultTbCoreDeviceRpcService implements TbCoreDeviceRpcService { entityNode.put(DataConstants.ADDITIONAL_INFO, msg.getAdditionalInfo()); try { - TbMsg tbMsg = TbMsg.newMsg(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, msg.getDeviceId(), Optional.ofNullable(currentUser).map(User::getCustomerId).orElse(null), metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); clusterService.pushMsgToRuleEngine(msg.getTenantId(), msg.getDeviceId(), tbMsg, null); } catch (IllegalArgumentException e) { throw new RuntimeException(e); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java index ad40176cef..8c6f0d768c 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/TbRpcService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rpc.Rpc; @@ -62,7 +63,7 @@ public class TbRpcService { } private void pushRpcMsgToRuleEngine(TenantId tenantId, Rpc rpc) { - TbMsg msg = TbMsg.newMsg("RPC_" + rpc.getStatus().name(), rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); + TbMsg msg = TbMsg.newMsg(TbMsgType.valueOf("RPC_" + rpc.getStatus().name()), rpc.getDeviceId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(rpc)); tbClusterService.pushMsgToRuleEngine(tenantId, rpc.getDeviceId(), msg, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 554359fd6d..8b09565ef9 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -36,7 +36,6 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.ApiUsageRecordKey; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.EntityType; @@ -51,6 +50,8 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.query.EntityData; @@ -63,7 +64,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -102,10 +102,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.DataConstants.ACTIVITY_EVENT; -import static org.thingsboard.server.common.data.DataConstants.CONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.DISCONNECT_EVENT; -import static org.thingsboard.server.common.data.DataConstants.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; /** @@ -229,7 +226,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES); future = Futures.transform(tsData, extractDeviceStateData(device), deviceStateExecutor); } else { - ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES); + ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), SERVER_SCOPE, PERSISTENT_ATTRIBUTES); future = Futures.transform(attrData, extractDeviceStateData(device), deviceStateExecutor); } return transformInactivityTimeout(future); @@ -771,11 +768,11 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, key, value)); } else { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); } } @@ -811,7 +808,7 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService(deviceId, key, value)); } else { - tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); + tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, SERVER_SCOPE, key, value, new TelemetrySaveCallback<>(deviceId, key, value)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index d0d996fe1f..89a7480708 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -53,6 +53,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.ota.OtaPackageUtil; import org.thingsboard.server.common.data.page.PageData; @@ -346,7 +347,7 @@ public class DefaultTransportApiService implements TransportApiService { DeviceId deviceId = device.getId(); JsonNode entityNode = JacksonUtil.valueToTree(device); - TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null); } else { JsonNode deviceAdditionalInfo = device.getAdditionalInfo(); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index c8dfd1b029..aa62a46d61 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -96,6 +96,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" cluster: stats: @@ -249,7 +250,8 @@ cassandra: concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}" permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}" dispatcher_threads: "${CASSANDRA_QUERY_DISPATCHER_THREADS:2}" - callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}" + callback_threads: "${CASSANDRA_QUERY_CALLBACK_THREADS:4}" # Buffered rate executor (read, write), for managing I/O rate. See "nosql-*-callback" threads in JMX + result_processing_threads: "${CASSANDRA_QUERY_RESULT_PROCESSING_THREADS:50}" # Result set transformer and processing. See "cassandra-callback" threads in JMX poll_ms: "${CASSANDRA_QUERY_POLL_MS:50}" rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}" # set all data types values except target to null for the same ts on save @@ -985,6 +987,35 @@ transport: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" +# Device connectivity properties to publish telemetry +device: + connectivity: + http: + enabled: "${DEVICE_CONNECTIVITY_HTTP_ENABLED:true}" + host: "${DEVICE_CONNECTIVITY_HTTP_HOST:}" + port: "${DEVICE_CONNECTIVITY_HTTP_PORT:8080}" + https: + enabled: "${DEVICE_CONNECTIVITY_HTTPS_ENABLED:false}" + host: "${DEVICE_CONNECTIVITY_HTTPS_HOST:}" + port: "${DEVICE_CONNECTIVITY_HTTPS_PORT:443}" + mqtt: + enabled: "${DEVICE_CONNECTIVITY_MQTT_ENABLED:true}" + host: "${DEVICE_CONNECTIVITY_MQTT_HOST:}" + port: "${DEVICE_CONNECTIVITY_MQTT_PORT:1883}" + mqtts: + enabled: "${DEVICE_CONNECTIVITY_MQTTS_ENABLED:false}" + host: "${DEVICE_CONNECTIVITY_MQTTS_HOST:}" + port: "${DEVICE_CONNECTIVITY_MQTTS_PORT:8883}" + pem_cert_file: "${DEVICE_CONNECTIVITY_MQTT_SSL_PEM_CERT:mqttserver.pem}" + coap: + enabled: "${DEVICE_CONNECTIVITY_COAP_ENABLED:true}" + host: "${DEVICE_CONNECTIVITY_COAP_HOST:}" + port: "${DEVICE_CONNECTIVITY_COAP_PORT:5683}" + coaps: + enabled: "${DEVICE_CONNECTIVITY_COAPS_ENABLED:false}" + host: "${DEVICE_CONNECTIVITY_COAPS_HOST:}" + port: "${DEVICE_CONNECTIVITY_COAPS_PORT:5684}" + # Edges parameters edges: enabled: "${EDGES_ENABLED:true}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java index 49e3550a1e..656936b175 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java @@ -25,13 +25,13 @@ import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.dao.rule.RuleChainService; -import java.io.IOException; import java.util.function.Predicate; /** @@ -82,8 +82,8 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle } } - protected Predicate filterByCustomEvent() { - return event -> event.getBody().get("msgType").textValue().equals("CUSTOM"); + protected Predicate filterByPostTelemetryEventType() { + return event -> event.getBody().get("msgType").textValue().equals(TbMsgType.POST_TELEMETRY_REQUEST.name()); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 50761be096..6ce6e22e9a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -531,6 +531,55 @@ public class AlarmControllerTest extends AbstractControllerTest { tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED); } + @Test + public void testUnassignTenantUserAlarmOnUserRemoving() throws Exception { + loginDifferentTenant(); + + User user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(tenantId); + user.setEmail("tenantForAssign@thingsboard.org"); + User savedUser = createUser(user, "password"); + + Device device = createDevice("Different tenant device", "default", "differentTenantTest"); + + Alarm alarm = Alarm.builder() + .type(TEST_ALARM_TYPE) + .tenantId(savedDifferentTenant.getId()) + .originator(device.getId()) + .severity(AlarmSeverity.MAJOR) + .build(); + alarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(alarm); + + alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(alarm); + + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + + beforeAssignmentTs = System.currentTimeMillis(); + + Mockito.reset(tbClusterService, auditLogService); + + loginSysAdmin(); + + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + + loginDifferentTenant(); + + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs); + } + @Test public void testUnassignAlarmOnUserRemoving() throws Exception { loginDifferentTenant(); diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java new file mode 100644 index 0000000000..36a4365544 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java @@ -0,0 +1,340 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.device.DeviceDao; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; + +@TestPropertySource(properties = { + "device.connectivity.https.enabled=true", + "device.connectivity.mqtts.enabled=true", + "device.connectivity.coaps.enabled=true", +}) +@ContextConfiguration(classes = {DeviceConnectivityControllerTest.Config.class}) +@DaoSqlTest +public class DeviceConnectivityControllerTest extends AbstractControllerTest { + static final TypeReference> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() { + }; + + private static final String DEVICE_TELEMETRY_TOPIC = "v1/devices/customTopic"; + private static final String CHECK_DOCUMENTATION = "Check documentation"; + + ListeningExecutorService executor; + + private Tenant savedTenant; + private User tenantAdmin; + private DeviceProfileId mqttDeviceProfileId; + private DeviceProfileId coapDeviceProfileId; + + static class Config { + @Bean + @Primary + public DeviceDao deviceDao(DeviceDao deviceDao) { + return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao)); + } + } + + @Before + public void beforeTest() throws Exception { + executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); + + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + DeviceProfile mqttProfile = new DeviceProfile(); + mqttProfile.setName("Mqtt device profile"); + mqttProfile.setType(DeviceProfileType.DEFAULT); + mqttProfile.setTransportType(DeviceTransportType.MQTT); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + deviceProfileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration(); + transportConfiguration.setDeviceTelemetryTopic(DEVICE_TELEMETRY_TOPIC); + deviceProfileData.setTransportConfiguration(transportConfiguration); + mqttProfile.setProfileData(deviceProfileData); + mqttProfile.setDefault(false); + mqttProfile.setDefaultRuleChainId(null); + + mqttDeviceProfileId = doPost("/api/deviceProfile", mqttProfile, DeviceProfile.class).getId(); + + DeviceProfile coapProfile = new DeviceProfile(); + coapProfile.setName("Coap device profile"); + coapProfile.setType(DeviceProfileType.DEFAULT); + coapProfile.setTransportType(DeviceTransportType.COAP); + DeviceProfileData deviceProfileData2 = new DeviceProfileData(); + deviceProfileData2.setConfiguration(new DefaultDeviceProfileConfiguration()); + deviceProfileData2.setTransportConfiguration(new CoapDeviceProfileTransportConfiguration()); + coapProfile.setProfileData(deviceProfileData); + coapProfile.setDefault(false); + coapProfile.setDefaultRuleChainId(null); + + coapDeviceProfileId = doPost("/api/deviceProfile", coapProfile, DeviceProfile.class).getId(); + } + + @After + public void afterTest() throws Exception { + executor.shutdownNow(); + + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId()) + .andExpect(status().isOk()); + } + + @Test + public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + assertThat(commands).hasSize(3); + JsonNode httpCommands = commands.get(HTTP); + assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " + + "--header Content-Type:application/json --data \"{temperature:25}\"", + credentials.getCredentialsId())); + + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t v1/devices/me/telemetry " + + "-u %s -m \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " + + "-t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", credentials.getCredentialsId())); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + " -p 1883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t v1/devices/me/telemetry -u %s -m \"{temperature:25}\"\"", + credentials.getCredentialsId())); + + JsonNode linuxCoapCommands = commands.get(COAP); + assertThat(linuxCoapCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry " + + "-t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + assertThat(linuxCoapCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry" + + " -t json -e \"{temperature:25}\"", credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForMqttDeviceWithAccessToken() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(mqttDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + + "-u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " + + "-t %s -u %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + " -p 1883 -t %s -u %s -m \"{temperature:25}\"", + DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -u %s -m \"{temperature:25}\"\"", + DEVICE_TELEMETRY_TOPIC, credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForDeviceWithMqttBasicCreds() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(mqttDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + credentials.setCredentialsId(null); + credentials.setCredentialsType(DeviceCredentialsType.MQTT_BASIC); + BasicMqttCredentials basicMqttCredentials = new BasicMqttCredentials(); + String clientId = "testClientId"; + String userName = "testUsername"; + String password = "testPassword"; + basicMqttCredentials.setClientId(clientId); + basicMqttCredentials.setUserName(userName); + basicMqttCredentials.setPassword(password); + credentials.setCredentialsValue(JacksonUtil.toString(basicMqttCredentials)); + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + JsonNode mqttCommands = commands.get(MQTT); + assertThat(mqttCommands.get(MQTT).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 -h localhost -p 1883 -t %s " + + "-i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + assertThat(mqttCommands.get(MQTTS).get(0).asText()).isEqualTo("curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download"); + assertThat(mqttCommands.get(MQTTS).get(1).asText()).isEqualTo(String.format("mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 " + + "-t %s -i %s -u %s -P %s -m \"{temperature:25}\"", DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + + JsonNode dockerMqttCommands = commands.get(MQTT).get(DOCKER); + assertThat(dockerMqttCommands.get(MQTT).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients mosquitto_pub -d -q 1 -h localhost" + + " -p 1883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"", + DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + assertThat(dockerMqttCommands.get(MQTTS).asText()).isEqualTo(String.format("docker run --rm -it thingsboard/mosquitto-clients " + + "/bin/sh -c \"curl -f -S -o tb-server-chain.pem http://localhost:80/api/device-connectivity/mqtts/certificate/download && " + + "mosquitto_pub -d -q 1 --cafile tb-server-chain.pem -h localhost -p 8883 -t %s -i %s -u %s -P %s -m \"{temperature:25}\"\"", + DEVICE_TELEMETRY_TOPIC, clientId, userName, password)); + } + + @Test + public void testFetchPublishTelemetryCommandsForDeviceWithX509Creds() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(mqttDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + credentials.setCredentialsId(null); + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + credentials.setCredentialsValue("testValue"); + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + assertThat(commands.get(MQTT).get(MQTTS).asText()).isEqualTo(CHECK_DOCUMENTATION); + assertThat(commands.get(MQTT).get(DOCKER)).isNull(); + } + + @Test + public void testFetchPublishTelemetryCommandsForCoapDevice() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(coapDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + + JsonNode linuxCommands = commands.get(COAP); + assertThat(linuxCommands.get(COAP).asText()).isEqualTo(String.format("coap-client -m POST coap://localhost:5683/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", + credentials.getCredentialsId())); + assertThat(linuxCommands.get(COAPS).asText()).isEqualTo(String.format("coap-client-openssl -m POST coaps://localhost:5684/api/v1/%s/telemetry -t json -e \"{temperature:25}\"", + credentials.getCredentialsId())); + } + + @Test + public void testFetchPublishTelemetryCommandsForCoapDeviceWithX509Creds() throws Exception { + Device device = new Device(); + device.setName("My device"); + device.setDeviceProfileId(coapDeviceProfileId); + + Device savedDevice = doPost("/api/device", device, Device.class); + DeviceCredentials credentials = + doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); + credentials.setCredentialsId(null); + credentials.setCredentialsType(DeviceCredentialsType.X509_CERTIFICATE); + credentials.setCredentialsValue("testValue"); + doPost("/api/device/credentials", credentials) + .andExpect(status().isOk()); + + JsonNode commands = + doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { + }); + assertThat(commands).hasSize(1); + assertThat(commands.get(COAP).get(COAPS).asText()).isEqualTo(CHECK_DOCUMENTATION); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index a65769f3f1..cb41019f6f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java @@ -43,6 +43,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.StringUtils; @@ -253,7 +254,7 @@ public class EntityViewControllerTest extends AbstractControllerTest { doGet("/api/entityView/" + entityIdStr) .andExpect(status().isNotFound()) - .andExpect(statusReason(containsString(msgErrorNoFound("Entity view",entityIdStr)))); + .andExpect(statusReason(containsString(msgErrorNoFound(EntityType.ENTITY_VIEW.getNormalName(), entityIdStr)))); } @Test @@ -425,12 +426,12 @@ public class EntityViewControllerTest extends AbstractControllerTest { testNotifyEntityBroadcastEntityStateChangeEventMany(new EntityView(), new EntityView(), tenantId, tenantAdminCustomerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, - ActionType.ADDED, ActionType.ADDED, cntEntity, 0, cntEntity*2, 0); + ActionType.ADDED, ActionType.ADDED, cntEntity, 0, cntEntity * 2, 0); testNotifyEntityBroadcastEntityStateChangeEventMany(new EntityView(), new EntityView(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ASSIGNED_TO_CUSTOMER, ActionType.ASSIGNED_TO_CUSTOMER, cntEntity, cntEntity, - cntEntity*2, 3); + cntEntity * 2, 3); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java new file mode 100644 index 0000000000..0394e8a505 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/plugin/TbWebSocketHandlerTest.java @@ -0,0 +1,160 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.adapter.NativeWebSocketSession; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.service.ws.WebSocketSessionRef; + +import javax.websocket.RemoteEndpoint; +import javax.websocket.SendHandler; +import javax.websocket.SendResult; +import javax.websocket.Session; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Slf4j +class TbWebSocketHandlerTest { + + TbWebSocketHandler wsHandler; + NativeWebSocketSession session; + Session nativeSession; + RemoteEndpoint.Async asyncRemote; + WebSocketSessionRef sessionRef; + int maxMsgQueuePerSession; + TbWebSocketHandler.SessionMetaData sendHandler; + ExecutorService executor; + + @BeforeEach + void setUp() throws IOException { + maxMsgQueuePerSession = 100; + executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getSimpleName())); + wsHandler = spy(new TbWebSocketHandler()); + willDoNothing().given(wsHandler).close(any(), any()); + session = mock(NativeWebSocketSession.class); + nativeSession = mock(Session.class); + willReturn(nativeSession).given(session).getNativeSession(Session.class); + asyncRemote = mock(RemoteEndpoint.Async.class); + willReturn(asyncRemote).given(nativeSession).getAsyncRemote(); + sessionRef = mock(WebSocketSessionRef.class, Mockito.RETURNS_DEEP_STUBS); //prevent NPE on logs + sendHandler = spy(wsHandler.new SessionMetaData(session, sessionRef, maxMsgQueuePerSession)); + } + + @AfterEach + void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + @Test + void sendHandler_sendMsg_parallel_no_race() throws InterruptedException { + CountDownLatch finishLatch = new CountDownLatch(maxMsgQueuePerSession * 2); + AtomicInteger sendersCount = new AtomicInteger(); + willAnswer(invocation -> { + assertThat(sendersCount.incrementAndGet()).as("no race").isEqualTo(1); + String text = invocation.getArgument(0); + SendHandler onResultHandler = invocation.getArgument(1); + SendResult sendResult = new SendResult(); + executor.submit(() -> { + sendersCount.decrementAndGet(); + onResultHandler.onResult(sendResult); + finishLatch.countDown(); + }); + return null; + }).given(asyncRemote).sendText(anyString(), any()); + + assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse(); + //first batch + IntStream.range(0, maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i)); + Awaitility.await("first batch processed").atMost(30, TimeUnit.SECONDS).until(() -> finishLatch.getCount() == maxMsgQueuePerSession); + assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse(); + //second batch - to test pause between big msg batches + IntStream.range(100, 100 + maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i)); + assertThat(finishLatch.await(30, TimeUnit.SECONDS)).as("all callbacks fired").isTrue(); + + verify(sendHandler, never()).closeSession(any()); + verify(sendHandler, times(maxMsgQueuePerSession * 2)).onResult(any()); + assertThat(sendHandler.isSending.get()).as("sendHandler not is in sending state").isFalse(); + } + + @Test + void sendHandler_sendMsg_message_order() throws InterruptedException { + CountDownLatch finishLatch = new CountDownLatch(maxMsgQueuePerSession); + Collection outputs = new ConcurrentLinkedQueue<>(); + willAnswer(invocation -> { + String text = invocation.getArgument(0); + outputs.add(text); + SendHandler onResultHandler = invocation.getArgument(1); + SendResult sendResult = new SendResult(); + executor.submit(() -> { + onResultHandler.onResult(sendResult); + finishLatch.countDown(); + }); + return null; + }).given(asyncRemote).sendText(anyString(), any()); + + List inputs = IntStream.range(0, maxMsgQueuePerSession).mapToObj(i -> "msg " + i).collect(Collectors.toList()); + inputs.forEach(s -> sendHandler.sendMsg(s)); + + assertThat(finishLatch.await(30, TimeUnit.SECONDS)).as("all callbacks fired").isTrue(); + assertThat(outputs).as("inputs exactly the same as outputs").containsExactlyElementsOf(inputs); + + verify(sendHandler, never()).closeSession(any()); + verify(sendHandler, times(maxMsgQueuePerSession)).onResult(any()); + } + + @Test + void sendHandler_sendMsg_queue_size_exceed() { + willDoNothing().given(asyncRemote).sendText(anyString(), any()); // send text will never call back, so queue will grow each sendMsg + sendHandler.sendMsg("first message to stay in-flight all the time during this test"); + IntStream.range(0, maxMsgQueuePerSession).parallel().forEach(i -> sendHandler.sendMsg("hello " + i)); + verify(sendHandler, never()).closeSession(any()); + sendHandler.sendMsg("excessive message"); + verify(sendHandler, times(1)).closeSession(eq(new CloseStatus(1008, "Max pending updates limit reached!"))); + verify(asyncRemote, times(1)).sendText(anyString(), any()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java index 6c2e5e9c5e..b86287c5d7 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java @@ -27,7 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; -import org.thingsboard.rule.engine.metadata.FetchTo; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.actors.ActorSystemContext; @@ -39,6 +39,8 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; @@ -142,7 +144,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setDebugMode(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); - configuration1.setFetchTo(FetchTo.METADATA); + configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -152,14 +154,14 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setDebugMode(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); - configuration2.setFetchTo(FetchTo.METADATA); + configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); metaData.setFirstNodeIndex(0); - metaData.addConnectionInfo(0, 1, "Success"); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); metaData = saveRuleChainMetaData(metaData); Assert.assertNotNull(metaData); @@ -179,14 +181,14 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); - TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system actorSystem.tell(qMsg); Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); PageData eventsPage = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000); - List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + List events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); EventInfo inEvent = events.stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get(); @@ -203,7 +205,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule RuleNode lastRuleNode = metaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get(); eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000); - events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); @@ -248,7 +250,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode1.setDebugMode(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); - configuration1.setFetchTo(FetchTo.METADATA); + configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -265,7 +267,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule NodeConnectionInfo connection = new NodeConnectionInfo(); connection.setFromIndex(0); connection.setToIndex(1); - connection.setType("Success"); + connection.setType(TbNodeConnectionType.SUCCESS); rootMetaData.setConnections(Collections.singletonList(connection)); rootMetaData = saveRuleChainMetaData(rootMetaData); Assert.assertNotNull(rootMetaData); @@ -282,7 +284,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode2.setDebugMode(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); - configuration2.setFetchTo(FetchTo.METADATA); + configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); @@ -304,7 +306,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); - TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(savedTenant.getId(), tbMsg, null, null); // Pushing Message to the system actorSystem.tell(qMsg); @@ -312,7 +314,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule Mockito.verify(tbMsgCallback, Mockito.timeout(10000)).onSuccess(); PageData eventsPage = getDebugEvents(savedTenant.getId(), rootRuleChain.getFirstRuleNodeId(), 1000); - List events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + List events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); @@ -330,7 +332,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule RuleNode lastRuleNode = secondaryMetaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get(); eventsPage = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000); - events = eventsPage.getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + events = eventsPage.getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); Assert.assertEquals(2, events.size()); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 9753f06aff..9b05be4a05 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.metadata.FetchTo; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.server.actors.ActorSystemContext; @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -97,7 +98,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); ruleNode.setDebugMode(true); TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey")); ruleNode.setConfiguration(JacksonUtil.valueToTree(configuration)); @@ -139,7 +140,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac log.warn("attr updated"); TbMsgCallback tbMsgCallback = Mockito.mock(TbMsgCallback.class); Mockito.when(tbMsgCallback.isMsgValid()).thenReturn(true); - TbMsg tbMsg = TbMsg.newMsg("CUSTOM", device.getId(), new TbMsgMetaData(), "{}", tbMsgCallback); + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, tbMsgCallback); QueueToRuleEngineMsg qMsg = new QueueToRuleEngineMsg(tenantId, tbMsg, null, null); // Pushing Message to the system log.warn("before tell tbMsgCallback"); @@ -147,12 +148,12 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac log.warn("awaiting tbMsgCallback"); Mockito.verify(tbMsgCallback, Mockito.timeout(TimeUnit.SECONDS.toMillis(TIMEOUT))).onSuccess(); log.warn("awaiting events"); - List events = Awaitility.await("get debug by custom event") + List events = Awaitility.await("get debug by post telemetry event") .pollInterval(10, MILLISECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS) .until(() -> { List debugEvents = getDebugEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), 1000) - .getData().stream().filter(filterByCustomEvent()).collect(Collectors.toList()); + .getData().stream().filter(filterByPostTelemetryEventType()).collect(Collectors.toList()); log.warn("filtered debug events [{}]", debugEvents.size()); debugEvents.forEach((e) -> log.warn("event: {}", e)); return debugEvents; diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java index dc3cc84c72..c1f34217ac 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/constructor/RuleChainMsgConstructorTest.java @@ -24,9 +24,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -45,6 +47,8 @@ import java.util.UUID; @RunWith(MockitoJUnitRunner.class) public class RuleChainMsgConstructorTest { + private static final String RPC_CONNECTION_TYPE = "RPC"; + private RuleChainMsgConstructor constructor; private TenantId tenantId; @@ -99,19 +103,19 @@ public class RuleChainMsgConstructorTest { Assert.assertEquals("Connections count incorrect!", 13, ruleChainMetadataUpdateMsg.getConnectionsCount()); Assert.assertEquals("Rule chain connections count incorrect!", 0, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, "Success"), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 11, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 7, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 4, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 5, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 8, "Other"), ruleChainMetadataUpdateMsg.getConnections(9)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(10)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 11, "Success"), ruleChainMetadataUpdateMsg.getConnections(11)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(10, 9, "RPC"), ruleChainMetadataUpdateMsg.getConnections(12)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 11, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 7, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 4, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 8, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(10)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 11, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(11)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(10, 9, RPC_CONNECTION_TYPE), ruleChainMetadataUpdateMsg.getConnections(12)); } @Test @@ -130,20 +134,20 @@ public class RuleChainMsgConstructorTest { Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 9, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 6, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 3, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 4, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 7, "Other"), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 8, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, "Success"), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(2, 5, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 9, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 6, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 3, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 4, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 7, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 8, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 9, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); Assert.assertEquals("From index incorrect!", 2, ruleChainConnection.getFromIndex()); - Assert.assertEquals("Type index incorrect!", "Success", ruleChainConnection.getType()); + Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType()); Assert.assertEquals("Additional info incorrect!", "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", ruleChainConnection.getAdditionalInfo()); @@ -172,20 +176,20 @@ public class RuleChainMsgConstructorTest { Assert.assertEquals("Connections count incorrect!", 10, ruleChainMetadataUpdateMsg.getConnectionsCount()); Assert.assertEquals("Rule chain connections count incorrect!", 1, ruleChainMetadataUpdateMsg.getRuleChainConnectionsCount()); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(0)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, "Attributes Updated"), ruleChainMetadataUpdateMsg.getConnections(1)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 3, "RPC Request from Device"), ruleChainMetadataUpdateMsg.getConnections(2)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 6, "Post telemetry"), ruleChainMetadataUpdateMsg.getConnections(3)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 5, "Post attributes"), ruleChainMetadataUpdateMsg.getConnections(4)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 2, "Other"), ruleChainMetadataUpdateMsg.getConnections(5)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 1, "RPC Request to Device"), ruleChainMetadataUpdateMsg.getConnections(6)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(7)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 0, "Success"), ruleChainMetadataUpdateMsg.getConnections(8)); - compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, "Success"), ruleChainMetadataUpdateMsg.getConnections(9)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(0)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 0, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(1)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 3, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(2)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 6, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(3)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(4)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 2, TbNodeConnectionType.OTHER), ruleChainMetadataUpdateMsg.getConnections(5)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(4, 1, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection()), ruleChainMetadataUpdateMsg.getConnections(6)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(5, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(7)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(6, 0, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(8)); + compareNodeConnectionInfoAndProto(createNodeConnectionInfo(7, 4, TbNodeConnectionType.SUCCESS), ruleChainMetadataUpdateMsg.getConnections(9)); RuleChainConnectionInfoProto ruleChainConnection = ruleChainMetadataUpdateMsg.getRuleChainConnections(0); Assert.assertEquals("From index incorrect!", 7, ruleChainConnection.getFromIndex()); - Assert.assertEquals("Type index incorrect!", "Success", ruleChainConnection.getType()); + Assert.assertEquals("Type index incorrect!", TbNodeConnectionType.SUCCESS, ruleChainConnection.getType()); Assert.assertEquals("Additional info incorrect!", "{\"description\":\"\",\"layoutX\":477,\"layoutY\":560,\"ruleChainNodeId\":\"rule-chain-node-UNDEFINED\"}", ruleChainConnection.getAdditionalInfo()); @@ -225,19 +229,19 @@ public class RuleChainMsgConstructorTest { private List createConnections() { List result = new ArrayList<>(); - result.add(createNodeConnectionInfo(3, 6, "Success")); - result.add(createNodeConnectionInfo(3, 10, "Success")); - result.add(createNodeConnectionInfo(3, 0, "Success")); - result.add(createNodeConnectionInfo(4, 11, "Success")); - result.add(createNodeConnectionInfo(5, 11, "Success")); - result.add(createNodeConnectionInfo(6, 11, "Attributes Updated")); - result.add(createNodeConnectionInfo(6, 7, "RPC Request from Device")); - result.add(createNodeConnectionInfo(6, 4, "Post telemetry")); - result.add(createNodeConnectionInfo(6, 5, "Post attributes")); - result.add(createNodeConnectionInfo(6, 8, "Other")); - result.add(createNodeConnectionInfo(6, 9, "RPC Request to Device")); - result.add(createNodeConnectionInfo(7, 11, "Success")); - result.add(createNodeConnectionInfo(10, 9, "RPC")); + result.add(createNodeConnectionInfo(3, 6, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(3, 10, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(3, 0, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(4, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(5, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(6, 11, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 7, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 4, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 5, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 8, TbNodeConnectionType.OTHER)); + result.add(createNodeConnectionInfo(6, 9, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(7, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(10, 9, RPC_CONNECTION_TYPE)); return result; } @@ -280,19 +284,19 @@ public class RuleChainMsgConstructorTest { private List createConnectionsInDifferentOrder() { List result = new ArrayList<>(); - result.add(createNodeConnectionInfo(0, 2, "RPC")); - result.add(createNodeConnectionInfo(4, 1, "Success")); - result.add(createNodeConnectionInfo(5, 1, "Attributes Updated")); - result.add(createNodeConnectionInfo(5, 4, "RPC Request from Device")); - result.add(createNodeConnectionInfo(5, 7, "Post telemetry")); - result.add(createNodeConnectionInfo(5, 6, "Post attributes")); - result.add(createNodeConnectionInfo(5, 3, "Other")); - result.add(createNodeConnectionInfo(5, 2, "RPC Request to Device")); - result.add(createNodeConnectionInfo(6, 1, "Success")); - result.add(createNodeConnectionInfo(7, 1, "Success")); - result.add(createNodeConnectionInfo(8, 11, "Success")); - result.add(createNodeConnectionInfo(8, 5, "Success")); - result.add(createNodeConnectionInfo(8, 0, "Success")); + result.add(createNodeConnectionInfo(0, 2, RPC_CONNECTION_TYPE)); + result.add(createNodeConnectionInfo(4, 1, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(5, 1, TbMsgType.ATTRIBUTES_UPDATED.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 4, TbMsgType.TO_SERVER_RPC_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 7, TbMsgType.POST_TELEMETRY_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 6, TbMsgType.POST_ATTRIBUTES_REQUEST.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(5, 3, TbNodeConnectionType.OTHER)); + result.add(createNodeConnectionInfo(5, 2, TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE.getRuleNodeConnection())); + result.add(createNodeConnectionInfo(6, 1, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(7, 1, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(8, 11, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(8, 5, TbNodeConnectionType.SUCCESS)); + result.add(createNodeConnectionInfo(8, 0, TbNodeConnectionType.SUCCESS)); return result; } diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java index 4373a4c108..5cd54a24eb 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsInvokeServiceTest.java @@ -33,6 +33,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @DaoSqlTest @TestPropertySource(properties = { @@ -121,7 +122,7 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest { } private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException { - return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString(); + return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index ca5dbef5f1..62af9cb16e 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -42,6 +42,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @DaoSqlTest @TestPropertySource(properties = { @@ -216,7 +217,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest { private String invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException { var msg = JacksonUtil.fromString(str, Map.class); - return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString(); + return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index e70a888194..654f934ed2 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -33,11 +33,11 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -133,7 +133,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { for (Device device : devices) { - TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), + TbMsg tbMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, device.getId(), getTbMsgMetadata(device.getName(), ts.get(idx)), TbMsgDataType.JSON, diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java index 39b61c6c47..b9b023e9e9 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java @@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -343,7 +344,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); metaData.setFirstNodeIndex(0); - metaData.addConnectionInfo(0, 1, "Success"); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); return ruleChainService.findRuleChainById(tenantId, ruleChain.getId()); @@ -381,7 +382,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2)); metaData.setFirstNodeIndex(0); - metaData.addConnectionInfo(0, 1, "Success"); + metaData.addConnectionInfo(0, 1, TbNodeConnectionType.SUCCESS); ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); return ruleChainService.findRuleChainById(tenantId, ruleChain.getId()); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java new file mode 100644 index 0000000000..90355d885d --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.core.io.Resource; +import org.thingsboard.server.common.data.Device; + +import java.net.URISyntaxException; + +public interface DeviceConnectivityService { + + JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException; + + Resource getPemCertFile(String protocol); +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 3ddb6187ea..39e94b3a6b 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -53,6 +53,8 @@ public interface RelationService { void deleteEntityRelations(TenantId tenantId, EntityId entity); + void deleteEntityCommonRelations(TenantId tenantId, EntityId entity); + List findByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); ListenableFuture> findByFromAsync(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 0f0cf0467f..02871a59b6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -109,6 +109,7 @@ public class DataConstants { public static final String PROVISION_KEY = "provisionDeviceKey"; public static final String PROVISION_SECRET = "provisionDeviceSecret"; + public static final String DEFAULT_DEVICE_TYPE = "default"; public static final String DEVICE_NAME = "deviceName"; public static final String DEVICE_TYPE = "deviceType"; public static final String CERT_PUB_KEY = "x509CertPubKey"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java index c88a6b970f..db069d8d61 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java @@ -68,20 +68,22 @@ public class EntityFieldsData { break; } } - if (current != null) { - if(current.isNull() && ignoreNullStrings){ + if (current == null) { + return null; + } + if (current.isNull() && ignoreNullStrings) { + return null; + } + if (current.isValueNode()) { + String textValue = current.asText(); + if (StringUtils.isEmpty(textValue) && ignoreNullStrings) { return null; } - if (current.isValueNode()) { - return current.asText(); - } else { - try { - return mapper.writeValueAsString(current); - } catch (JsonProcessingException e) { - return null; - } - } - } else { + return textValue; + } + try { + return mapper.writeValueAsString(current); + } catch (JsonProcessingException e) { return null; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 572dc16c0b..8ca6585718 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -18,6 +18,10 @@ package org.thingsboard.server.common.data; import lombok.Getter; import org.apache.commons.lang3.StringUtils; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; + /** * @author Andrew Shvayka */ @@ -31,7 +35,13 @@ public enum EntityType { ALARM, RULE_CHAIN, RULE_NODE, - ENTITY_VIEW, + ENTITY_VIEW { + // backward compatibility for TbOriginatorTypeSwitchNode to return correct rule node connection. + @Override + public String getNormalName() { + return "Entity View"; + } + }, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, @@ -49,6 +59,9 @@ public enum EntityType { NOTIFICATION, NOTIFICATION_RULE; + public static final List NORMAL_NAMES = EnumSet.allOf(EntityType.class).stream() + .map(EntityType::getNormalName).collect(Collectors.toUnmodifiableList()); + @Getter private final String normalName = StringUtils.capitalize(StringUtils.removeStart(name(), "TB_") .toLowerCase().replaceAll("_", " ")); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 01009751d4..056be5a958 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -16,49 +16,61 @@ package org.thingsboard.server.common.data.audit; import lombok.Getter; +import org.thingsboard.server.common.data.msg.TbMsgType; + +import java.util.Optional; -@Getter public enum ActionType { - ADDED(false), // log entity - DELETED(false), // log string id - UPDATED(false), // log entity - ATTRIBUTES_UPDATED(false), // log attributes/values - ATTRIBUTES_DELETED(false), // log attributes - TIMESERIES_UPDATED(false), // log timeseries update - TIMESERIES_DELETED(false), // log timeseries - RPC_CALL(false), // log method and params - CREDENTIALS_UPDATED(false), // log new credentials - ASSIGNED_TO_CUSTOMER(false), // log customer name - UNASSIGNED_FROM_CUSTOMER(false), // log customer name - ACTIVATED(false), // log string id - SUSPENDED(false), // log string id - CREDENTIALS_READ(true), // log device id - ATTRIBUTES_READ(true), // log attributes - RELATION_ADD_OR_UPDATE(false), - RELATION_DELETED(false), - RELATIONS_DELETED(false), - ALARM_ACK(false), - ALARM_CLEAR(false), - ALARM_DELETE(false), - ALARM_ASSIGNED(false), - ALARM_UNASSIGNED(false), - LOGIN(false), - LOGOUT(false), - LOCKOUT(false), - ASSIGNED_FROM_TENANT(false), - ASSIGNED_TO_TENANT(false), - PROVISION_SUCCESS(false), - PROVISION_FAILURE(false), - ASSIGNED_TO_EDGE(false), // log edge name - UNASSIGNED_FROM_EDGE(false), - ADDED_COMMENT(false), - UPDATED_COMMENT(false), - DELETED_COMMENT(false), - SMS_SENT(false); + ADDED(false, TbMsgType.ENTITY_CREATED), // log entity + DELETED(false, TbMsgType.ENTITY_DELETED), // log string id + UPDATED(false, TbMsgType.ENTITY_UPDATED), // log entity + ATTRIBUTES_UPDATED(false, TbMsgType.ATTRIBUTES_UPDATED), // log attributes/values + ATTRIBUTES_DELETED(false, TbMsgType.ATTRIBUTES_DELETED), // log attributes + TIMESERIES_UPDATED(false, TbMsgType.TIMESERIES_UPDATED), // log timeseries update + TIMESERIES_DELETED(false, TbMsgType.TIMESERIES_DELETED), // log timeseries + RPC_CALL(false, null), // log method and params + CREDENTIALS_UPDATED(false, null), // log new credentials + ASSIGNED_TO_CUSTOMER(false, TbMsgType.ENTITY_ASSIGNED), // log customer name + UNASSIGNED_FROM_CUSTOMER(false, TbMsgType.ENTITY_UNASSIGNED), // log customer name + ACTIVATED(false, null), // log string id + SUSPENDED(false, null), // log string id + CREDENTIALS_READ(true, null), // log device id + ATTRIBUTES_READ(true, null), // log attributes + RELATION_ADD_OR_UPDATE(false, TbMsgType.RELATION_ADD_OR_UPDATE), + RELATION_DELETED(false, TbMsgType.RELATION_DELETED), + RELATIONS_DELETED(false, TbMsgType.RELATIONS_DELETED), + ALARM_ACK(false, TbMsgType.ALARM_ACK), + ALARM_CLEAR(false, TbMsgType.ALARM_CLEAR), + ALARM_DELETE(false, TbMsgType.ALARM_DELETE), + ALARM_ASSIGNED(false, TbMsgType.ALARM_ASSIGNED), + ALARM_UNASSIGNED(false, TbMsgType.ALARM_UNASSIGNED), + LOGIN(false, null), + LOGOUT(false, null), + LOCKOUT(false, null), + ASSIGNED_FROM_TENANT(false, TbMsgType.ENTITY_ASSIGNED_FROM_TENANT), + ASSIGNED_TO_TENANT(false, TbMsgType.ENTITY_ASSIGNED_TO_TENANT), + PROVISION_SUCCESS(false, TbMsgType.PROVISION_SUCCESS), + PROVISION_FAILURE(false, TbMsgType.PROVISION_FAILURE), + ASSIGNED_TO_EDGE(false, TbMsgType.ENTITY_ASSIGNED_TO_EDGE), // log edge name + UNASSIGNED_FROM_EDGE(false, TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE), + ADDED_COMMENT(false, TbMsgType.COMMENT_CREATED), + UPDATED_COMMENT(false, TbMsgType.COMMENT_UPDATED), + DELETED_COMMENT(false, null), + SMS_SENT(false, null); + + @Getter private final boolean isRead; - ActionType(boolean isRead) { + private final TbMsgType ruleEngineMsgType; + + ActionType(boolean isRead, TbMsgType ruleEngineMsgType) { this.isRead = isRead; + this.ruleEngineMsgType = ruleEngineMsgType; } + + public Optional getRuleEngineMsgType() { + return Optional.ofNullable(ruleEngineMsgType); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java new file mode 100644 index 0000000000..206b203682 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbMsgType.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.msg; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public enum TbMsgType { + + POST_ATTRIBUTES_REQUEST("Post attributes"), + POST_TELEMETRY_REQUEST("Post telemetry"), + TO_SERVER_RPC_REQUEST("RPC Request from Device"), + ACTIVITY_EVENT("Activity Event"), + INACTIVITY_EVENT("Inactivity Event"), + CONNECT_EVENT("Connect Event"), + DISCONNECT_EVENT("Disconnect Event"), + ENTITY_CREATED("Entity Created"), + ENTITY_UPDATED("Entity Updated"), + ENTITY_DELETED("Entity Deleted"), + ENTITY_ASSIGNED("Entity Assigned"), + ENTITY_UNASSIGNED("Entity Unassigned"), + ATTRIBUTES_UPDATED("Attributes Updated"), + ATTRIBUTES_DELETED("Attributes Deleted"), + ALARM(null), + ALARM_ACK("Alarm Acknowledged"), + ALARM_CLEAR("Alarm Cleared"), + ALARM_DELETE(null), + ALARM_ASSIGNED("Alarm Assigned"), + ALARM_UNASSIGNED("Alarm Unassigned"), + COMMENT_CREATED("Comment Created"), + COMMENT_UPDATED("Comment Updated"), + RPC_CALL_FROM_SERVER_TO_DEVICE("RPC Request to Device"), + ENTITY_ASSIGNED_FROM_TENANT("Entity Assigned From Tenant"), + ENTITY_ASSIGNED_TO_TENANT("Entity Assigned To Tenant"), + ENTITY_ASSIGNED_TO_EDGE(null), + ENTITY_UNASSIGNED_FROM_EDGE(null), + TIMESERIES_UPDATED("Timeseries Updated"), + TIMESERIES_DELETED("Timeseries Deleted"), + RPC_QUEUED("RPC Queued"), + RPC_SENT("RPC Sent"), + RPC_DELIVERED("RPC Delivered"), + RPC_SUCCESSFUL("RPC Successful"), + RPC_TIMEOUT("RPC Timeout"), + RPC_EXPIRED("RPC Expired"), + RPC_FAILED("RPC Failed"), + RPC_DELETED("RPC Deleted"), + RELATION_ADD_OR_UPDATE("Relation Added or Updated"), + RELATION_DELETED("Relation Deleted"), + RELATIONS_DELETED("All Relations Deleted"), + PROVISION_SUCCESS(null), + PROVISION_FAILURE(null), + SEND_EMAIL(null), + + // tellSelfOnly types + GENERATOR_NODE_SELF_MSG(null, true), + DEVICE_PROFILE_PERIODIC_SELF_MSG(null, true), + DEVICE_PROFILE_UPDATE_SELF_MSG(null, true), + DEVICE_UPDATE_SELF_MSG(null, true), + DEDUPLICATION_TIMEOUT_SELF_MSG(null, true), + DELAY_TIMEOUT_SELF_MSG(null, true), + MSG_COUNT_SELF_MSG(null, true); + + public static final List NODE_CONNECTIONS = EnumSet.allOf(TbMsgType.class).stream() + .filter(tbMsgType -> !tbMsgType.isTellSelfOnly()) + .map(TbMsgType::getRuleNodeConnection) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + + @Getter + private final String ruleNodeConnection; + + @Getter + private final boolean tellSelfOnly; + + TbMsgType(String ruleNodeConnection, boolean tellSelfOnly) { + this.ruleNodeConnection = ruleNodeConnection; + this.tellSelfOnly = tellSelfOnly; + } + + TbMsgType(String ruleNodeConnection) { + this.ruleNodeConnection = ruleNodeConnection; + this.tellSelfOnly = false; + } + + public static String getRuleNodeConnectionOrElseOther(String msgType) { + if (msgType == null) { + return TbNodeConnectionType.OTHER; + } else { + return Arrays.stream(TbMsgType.values()) + .filter(type -> type.name().equals(msgType)) + .findFirst() + .map(TbMsgType::getRuleNodeConnection) + .orElse(TbNodeConnectionType.OTHER); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java new file mode 100644 index 0000000000..584393bf53 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.msg; + +/** + * Created by ashvayka on 19.01.18. + */ +public final class TbNodeConnectionType { + + public static final String SUCCESS = "Success"; + public static final String FAILURE = "Failure"; + + public static final String TRUE = "True"; + public static final String FALSE = "False"; + + public static final String OTHER = "Other"; + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/EntityTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/EntityTypeTest.java new file mode 100644 index 0000000000..9eee9ec23d --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/EntityTypeTest.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class EntityTypeTest { + + // backward-compatibility test + @Test + void getNormalNameTest() { + assertThat(EntityType.ENTITY_VIEW.getNormalName()).isEqualTo("Entity View"); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java new file mode 100644 index 0000000000..8c602c3cff --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/audit/ActionTypeTest.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.audit; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.audit.ActionType.ACTIVATED; +import static org.thingsboard.server.common.data.audit.ActionType.ATTRIBUTES_READ; +import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_READ; +import static org.thingsboard.server.common.data.audit.ActionType.CREDENTIALS_UPDATED; +import static org.thingsboard.server.common.data.audit.ActionType.DELETED_COMMENT; +import static org.thingsboard.server.common.data.audit.ActionType.LOCKOUT; +import static org.thingsboard.server.common.data.audit.ActionType.LOGIN; +import static org.thingsboard.server.common.data.audit.ActionType.LOGOUT; +import static org.thingsboard.server.common.data.audit.ActionType.RPC_CALL; +import static org.thingsboard.server.common.data.audit.ActionType.SMS_SENT; +import static org.thingsboard.server.common.data.audit.ActionType.SUSPENDED; + +class ActionTypeTest { + + private static final List typesWithNullRuleEngineMsgType = List.of( + RPC_CALL, + CREDENTIALS_UPDATED, + ACTIVATED, + SUSPENDED, + CREDENTIALS_READ, + ATTRIBUTES_READ, + LOGIN, + LOGOUT, + LOCKOUT, + DELETED_COMMENT, + SMS_SENT + ); + + // backward-compatibility tests + + @Test + void getRuleEngineMsgTypeTest() { + var types = ActionType.values(); + for (var type : types) { + if (typesWithNullRuleEngineMsgType.contains(type)) { + assertThat(type.getRuleEngineMsgType()).isEmpty(); + } else { + assertThat(type.getRuleEngineMsgType()).isPresent(); + } + } + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java new file mode 100644 index 0000000000..a37eb31d72 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/msg/TbMsgTypeTest.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.msg; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DELAY_TIMEOUT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED_TO_EDGE; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED_FROM_EDGE; +import static org.thingsboard.server.common.data.msg.TbMsgType.MSG_COUNT_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_FAILURE; +import static org.thingsboard.server.common.data.msg.TbMsgType.PROVISION_SUCCESS; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.DEVICE_UPDATE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.GENERATOR_NODE_SELF_MSG; +import static org.thingsboard.server.common.data.msg.TbMsgType.SEND_EMAIL; + +class TbMsgTypeTest { + + private static final List typesWithNullRuleNodeConnection = List.of( + ALARM, + ALARM_DELETE, + ENTITY_ASSIGNED_TO_EDGE, + ENTITY_UNASSIGNED_FROM_EDGE, + PROVISION_FAILURE, + PROVISION_SUCCESS, + SEND_EMAIL, + GENERATOR_NODE_SELF_MSG, + DEVICE_PROFILE_PERIODIC_SELF_MSG, + DEVICE_PROFILE_UPDATE_SELF_MSG, + DEVICE_UPDATE_SELF_MSG, + DEDUPLICATION_TIMEOUT_SELF_MSG, + DELAY_TIMEOUT_SELF_MSG, + MSG_COUNT_SELF_MSG + ); + + // backward-compatibility tests + + @Test + void getRuleNodeConnectionsTest() { + var tbMsgTypes = TbMsgType.values(); + for (var type : tbMsgTypes) { + if (typesWithNullRuleNodeConnection.contains(type)) { + assertThat(type.getRuleNodeConnection()).isNull(); + } else { + assertThat(type.getRuleNodeConnection()).isNotNull(); + } + } + } + + @Test + void getRuleNodeConnectionOrElseOtherTest() { + assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(null)) + .isEqualTo(TbNodeConnectionType.OTHER); + var tbMsgTypes = TbMsgType.values(); + for (var type : tbMsgTypes) { + if (typesWithNullRuleNodeConnection.contains(type)) { + assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())) + .isEqualTo(TbNodeConnectionType.OTHER); + } else { + assertThat(TbMsgType.getRuleNodeConnectionOrElseOther(type.name())).isNotNull() + .isNotEqualTo(TbNodeConnectionType.OTHER); + } + } + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 17bc7c0a8f..a987a4a253 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -29,10 +29,12 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.gen.MsgProtos; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.io.Serializable; +import java.util.Objects; import java.util.UUID; /** @@ -42,6 +44,10 @@ import java.util.UUID; @Slf4j public final class TbMsg implements Serializable { + public static final String EMPTY_JSON_OBJECT = "{}"; + public static final String EMPTY_JSON_ARRAY = "[]"; + public static final String EMPTY_STRING = ""; + private final String queueName; private final UUID id; private final long ts; @@ -66,82 +72,234 @@ public final class TbMsg implements Serializable { return ctx.getAndIncrementRuleNodeCounter(); } + @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return newMsg(queueName, type, originator, null, metaData, data, ruleChainId, ruleNodeId); } + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.5.2 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, CustomerId, TbMsgMetaData, String, RuleChainId, RuleNodeId)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param customerId the ID of the customer associated with the message + * @param metaData the metadata of the message + * @param data the data of the message + * @param ruleChainId the ID of the rule chain associated with the message + * @param ruleNodeId the ID of the rule node associated with the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.5.2") public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } + @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) { return newMsg(type, originator, null, metaData, data); } + @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return newMsg(queueName, type, originator, null, metaData, data, ruleChainId, ruleNodeId); + } + + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(type, originator, null, metaData, data); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); + } + // REALLY NEW MSG + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.5.2 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, TbMsgMetaData, String)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param metaData the metadata of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.5.2") public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data) { return newMsg(queueName, type, originator, null, metaData, data); } + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.5.2 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, CustomerId, TbMsgMetaData, String)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param customerId the ID of the customer associated with the message + * @param metaData the metadata of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.5.2") public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } + @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); } + /** + * Creates a new TbMsg instance with the specified parameters. + * + *

Deprecated: This method is deprecated since version 3.5.2 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(TbMsgType, EntityId, TbMsgMetaData, TbMsgDataType, String)} + * method instead.

+ * + * @param type the type of the message + * @param originator the originator of the message + * @param metaData the metadata of the message + * @param dataType the dataType of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.5.2") public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { return newMsg(type, originator, null, metaData, dataType, data); } + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return newMsg(queueName, type, originator, null, metaData, data); + } + + public static TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, customerId, + metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { + return newMsg(type, originator, null, metaData, dataType, data); + } + // For Tests only + @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } + @Deprecated(since = "3.5.2", forRemoval = true) public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); } + /** + * Transforms an existing TbMsg instance by changing its message type, originator, metadata, and data. + * + *

Deprecated: This method is deprecated since version 3.5.2 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #transformMsg(TbMsg, TbMsgType, EntityId, TbMsgMetaData, String)} + * method instead.

+ * + * + * @param tbMsg the TbMsg instance to transform + * @param type the new message type + * @param originator the new originator + * @param metaData the new metadata + * @param data the new data + * @return the transformed TbMsg instance + */ + @Deprecated(since = "3.5.2") public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); } + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null, + metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); + } + + public static TbMsg newMsg(TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { + return new TbMsg(null, UUID.randomUUID(), System.currentTimeMillis(), type.name(), originator, null, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); + } + + public static TbMsg transformMsg(TbMsg tbMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type.name(), originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); + } + + public static TbMsg transformMsgOriginator(TbMsg tbMsg, EntityId originatorId) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, originatorId, tbMsg.getCustomerId(), tbMsg.metaData, tbMsg.dataType, + tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + } + public static TbMsg transformMsgData(TbMsg tbMsg, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata) { + public static TbMsg transformMsgMetadata(TbMsg tbMsg, TbMsgMetaData metadata) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, CustomerId customerId) { + public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata, String data) { + return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata, tbMsg.dataType, + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); + } + + public static TbMsg transformMsgCustomerId(TbMsg tbMsg, CustomerId customerId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId) { + public static TbMsg transformMsgRuleChainId(TbMsg tbMsg, RuleChainId ruleChainId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } - public static TbMsg transformMsg(TbMsg tbMsg, String queueName) { + public static TbMsg transformMsgQueueName(TbMsg tbMsg, String queueName) { return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); } @@ -183,11 +341,7 @@ public final class TbMsg implements Serializable { this.ruleChainId = ruleChainId; this.ruleNodeId = ruleNodeId; this.ctx = ctx != null ? ctx : new TbMsgProcessingCtx(); - if (callback != null) { - this.callback = callback; - } else { - this.callback = TbMsgCallback.EMPTY; - } + this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } public static ByteString toByteString(TbMsg msg) { @@ -284,11 +438,7 @@ public final class TbMsg implements Serializable { public TbMsgCallback getCallback() { // May be null in case of deserialization; - if (callback != null) { - return callback; - } else { - return TbMsgCallback.EMPTY; - } + return Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } public void pushToStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { @@ -317,4 +467,18 @@ public final class TbMsg implements Serializable { } return ts; } + + public boolean isTypeOf(TbMsgType tbMsgType) { + return tbMsgType != null && tbMsgType.name().equals(this.type); + } + + public boolean isTypeOneOf(TbMsgType... types) { + for (TbMsgType type : types) { + if (isTypeOf(type)) { + return true; + } + } + return false; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java index dfcf178473..ca7c94e9ce 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsgType.java @@ -15,6 +15,12 @@ */ package org.thingsboard.server.common.msg.session; +/** + * @deprecated This enum is deprecated and will be removed in a future version. + * Note: This enum was originally part of the public API but is now specific to CoAP transport only. + * Please use {@link org.thingsboard.server.transport.coap.CoapSessionMsgType} instead. + */ +@Deprecated(since="3.5.2", forRemoval = true) public enum SessionMsgType { GET_ATTRIBUTES_REQUEST(true), POST_ATTRIBUTES_REQUEST(true), GET_ATTRIBUTES_RESPONSE, SUBSCRIBE_ATTRIBUTES_REQUEST, UNSUBSCRIBE_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATE_NOTIFICATION, diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java index fcf80bcf3d..44999d016a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ZkDiscoveryService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.queue.discovery; import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ProtocolStringList; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.curator.framework.CuratorFramework; @@ -44,8 +45,10 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.List; import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -66,6 +69,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi private Integer zkSessionTimeout; @Value("${zk.zk_dir}") private String zkDir; + @Value("${zk.recalculate_delay:60000}") + private Long recalculateDelay; + + protected final ConcurrentHashMap> delayedTasks; private final TbServiceInfoProvider serviceInfoProvider; private final PartitionService partitionService; @@ -82,6 +89,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi PartitionService partitionService) { this.serviceInfoProvider = serviceInfoProvider; this.partitionService = partitionService; + delayedTasks = new ConcurrentHashMap<>(); } @PostConstruct @@ -287,11 +295,39 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi log.error("Failed to decode server instance for node {}", data.getPath(), e); throw e; } - log.debug("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), instance.getServiceId()); + + String serviceId = instance.getServiceId(); + ProtocolStringList serviceTypesList = instance.getServiceTypesList(); + + log.trace("Processing [{}] event for [{}]", pathChildrenCacheEvent.getType(), serviceId); switch (pathChildrenCacheEvent.getType()) { case CHILD_ADDED: + ScheduledFuture task = delayedTasks.remove(serviceId); + if (task != null) { + if (task.cancel(false)) { + log.debug("[{}] Recalculate partitions ignored. Service was restarted in time [{}].", + serviceId, serviceTypesList); + } else { + log.debug("[{}] Going to recalculate partitions. Service was not restarted in time [{}]!", + serviceId, serviceTypesList); + recalculatePartitions(); + } + } else { + log.trace("[{}] Going to recalculate partitions due to adding new node [{}].", + serviceId, serviceTypesList); + recalculatePartitions(); + } + break; case CHILD_REMOVED: - recalculatePartitions(); + ScheduledFuture future = zkExecutorService.schedule(() -> { + log.debug("[{}] Going to recalculate partitions due to removed node [{}]", + serviceId, serviceTypesList); + ScheduledFuture removedTask = delayedTasks.remove(serviceId); + if (removedTask != null) { + recalculatePartitions(); + } + }, recalculateDelay, TimeUnit.MILLISECONDS); + delayedTasks.put(serviceId, future); break; default: break; @@ -303,6 +339,8 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi * Synchronized to ensure that other servers info is up to date * */ synchronized void recalculatePartitions() { + delayedTasks.values().forEach(future -> future.cancel(false)); + delayedTasks.clear(); partitionService.recalculatePartitions(serviceInfoProvider.getServiceInfo(), getOtherServers()); } diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java new file mode 100644 index 0000000000..a8810efd0e --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/discovery/ZkDiscoveryServiceTest.java @@ -0,0 +1,189 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.queue.discovery; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.imps.CuratorFrameworkState; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.PathChildrenCache; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_ADDED; +import static org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type.CHILD_REMOVED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ZkDiscoveryServiceTest { + + @Mock + private TbServiceInfoProvider serviceInfoProvider; + + @Mock + private PartitionService partitionService; + + @Mock + private CuratorFramework client; + + @Mock + private PathChildrenCache cache; + + @Mock + private CuratorFramework curatorFramework; + + private ZkDiscoveryService zkDiscoveryService; + + private static final long RECALCULATE_DELAY = 100L; + + final TransportProtos.ServiceInfo currentInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-rule-engine-0").build(); + final ChildData currentData = new ChildData("/thingsboard/nodes/0000000010", null, currentInfo.toByteArray()); + final TransportProtos.ServiceInfo childInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-rule-engine-1").build(); + final ChildData childData = new ChildData("/thingsboard/nodes/0000000020", null, childInfo.toByteArray()); + + @Before + public void setup() { + zkDiscoveryService = Mockito.spy(new ZkDiscoveryService(serviceInfoProvider, partitionService)); + ScheduledExecutorService zkExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("zk-discovery")); + when(client.getState()).thenReturn(CuratorFrameworkState.STARTED); + ReflectionTestUtils.setField(zkDiscoveryService, "stopped", false); + ReflectionTestUtils.setField(zkDiscoveryService, "client", client); + ReflectionTestUtils.setField(zkDiscoveryService, "cache", cache); + ReflectionTestUtils.setField(zkDiscoveryService, "nodePath", "/thingsboard/nodes/0000000010"); + ReflectionTestUtils.setField(zkDiscoveryService, "zkExecutorService", zkExecutorService); + ReflectionTestUtils.setField(zkDiscoveryService, "recalculateDelay", RECALCULATE_DELAY); + ReflectionTestUtils.setField(zkDiscoveryService, "zkDir", "/thingsboard"); + + when(serviceInfoProvider.getServiceInfo()).thenReturn(currentInfo); + + List dataList = new ArrayList<>(); + dataList.add(currentData); + when(cache.getCurrentData()).thenReturn(dataList); + } + + @Test + public void restartNodeInTimeTest() throws Exception { + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + + stopNode(childData); + + assertEquals(1, zkDiscoveryService.delayedTasks.size()); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + startNode(childData); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + Thread.sleep(RECALCULATE_DELAY * 2); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + assertTrue(zkDiscoveryService.delayedTasks.isEmpty()); + } + + @Test + public void restartNodeNotInTimeTest() throws Exception { + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + + stopNode(childData); + + assertEquals(1, zkDiscoveryService.delayedTasks.size()); + + Thread.sleep(RECALCULATE_DELAY * 2); + + assertTrue(zkDiscoveryService.delayedTasks.isEmpty()); + + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(Collections.emptyList())); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + } + + @Test + public void startAnotherNodeDuringRestartTest() throws Exception { + var anotherInfo = TransportProtos.ServiceInfo.newBuilder().setServiceId("tb-transport").build(); + var anotherData = new ChildData("/thingsboard/nodes/0000000030", null, anotherInfo.toByteArray()); + + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(childInfo))); + + reset(partitionService); + + stopNode(childData); + + assertEquals(1, zkDiscoveryService.delayedTasks.size()); + + startNode(anotherData); + + assertTrue(zkDiscoveryService.delayedTasks.isEmpty()); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(anotherInfo))); + reset(partitionService); + + Thread.sleep(RECALCULATE_DELAY * 2); + + verify(partitionService, never()).recalculatePartitions(any(), any()); + + startNode(childData); + + verify(partitionService, times(1)).recalculatePartitions(eq(currentInfo), eq(List.of(anotherInfo, childInfo))); + } + + private void startNode(ChildData data) throws Exception { + cache.getCurrentData().add(data); + zkDiscoveryService.childEvent(curatorFramework, new PathChildrenCacheEvent(CHILD_ADDED, data)); + } + + private void stopNode(ChildData data) throws Exception { + cache.getCurrentData().remove(data); + zkDiscoveryService.childEvent(curatorFramework, new PathChildrenCacheEvent(CHILD_REMOVED, data)); + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java index bbf441a659..2a60980f84 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java @@ -66,8 +66,6 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem protected final Map scriptIdToHash = new ConcurrentHashMap<>(); protected final Map scriptMap = new ConcurrentHashMap<>(); - private final String tbelSwitch = "switch"; - private final String tbelSwitchErrorMsg = "TBEL does not support the 'switch'."; protected Cache compiledScriptsCache; private SandboxedParserConfiguration parserConfig; @@ -183,11 +181,6 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem lock.unlock(); } return scriptId; - } catch (CompileException ce) { - if ( ce.getExpr() != null && new String(ce.getExpr()).contains(tbelSwitch)) { - ce = new CompileException(tbelSwitchErrorMsg, ce.getExpr(), ce.getCursor(), ce.getCause()); - } - throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, ce); } catch (Exception e) { throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapSessionMsgType.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapSessionMsgType.java new file mode 100644 index 0000000000..fcf33ea037 --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapSessionMsgType.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.coap; + +public enum CoapSessionMsgType { + + GET_ATTRIBUTES_REQUEST, + POST_ATTRIBUTES_REQUEST, + SUBSCRIBE_ATTRIBUTES_REQUEST, + UNSUBSCRIBE_ATTRIBUTES_REQUEST, + POST_TELEMETRY_REQUEST, + SUBSCRIBE_RPC_COMMANDS_REQUEST, + UNSUBSCRIBE_RPC_COMMANDS_REQUEST, + TO_DEVICE_RPC_RESPONSE, + TO_SERVER_RPC_REQUEST, + CLAIM_REQUEST; + +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 7dde25bfd0..d6958137c7 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; @@ -120,7 +119,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } else if (exchange.getRequestOptions().hasObserve()) { processExchangeGetRequest(exchange, featureType.get()); } else if (featureType.get() == FeatureType.ATTRIBUTES) { - processRequest(exchange, SessionMsgType.GET_ATTRIBUTES_REQUEST); + processRequest(exchange, CoapSessionMsgType.GET_ATTRIBUTES_REQUEST); } else { log.trace("Invalid feature type parameter"); exchange.respond(CoAP.ResponseCode.BAD_REQUEST); @@ -129,13 +128,13 @@ public class CoapTransportResource extends AbstractCoapTransportResource { private void processExchangeGetRequest(CoapExchange exchange, FeatureType featureType) { boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1; - SessionMsgType sessionMsgType; + CoapSessionMsgType coapSessionMsgType; if (featureType == FeatureType.RPC) { - sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : SessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST; + coapSessionMsgType = unsubscribe ? CoapSessionMsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : CoapSessionMsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST; } else { - sessionMsgType = unsubscribe ? SessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : SessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST; + coapSessionMsgType = unsubscribe ? CoapSessionMsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : CoapSessionMsgType.SUBSCRIBE_ATTRIBUTES_REQUEST; } - processRequest(exchange, sessionMsgType); + processRequest(exchange, coapSessionMsgType); } @Override @@ -147,21 +146,21 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } else { switch (featureType.get()) { case ATTRIBUTES: - processRequest(exchange, SessionMsgType.POST_ATTRIBUTES_REQUEST); + processRequest(exchange, CoapSessionMsgType.POST_ATTRIBUTES_REQUEST); break; case TELEMETRY: - processRequest(exchange, SessionMsgType.POST_TELEMETRY_REQUEST); + processRequest(exchange, CoapSessionMsgType.POST_TELEMETRY_REQUEST); break; case RPC: Optional requestId = getRequestId(exchange.advanced().getRequest()); if (requestId.isPresent()) { - processRequest(exchange, SessionMsgType.TO_DEVICE_RPC_RESPONSE); + processRequest(exchange, CoapSessionMsgType.TO_DEVICE_RPC_RESPONSE); } else { - processRequest(exchange, SessionMsgType.TO_SERVER_RPC_REQUEST); + processRequest(exchange, CoapSessionMsgType.TO_SERVER_RPC_REQUEST); } break; case CLAIM: - processRequest(exchange, SessionMsgType.CLAIM_REQUEST); + processRequest(exchange, CoapSessionMsgType.CLAIM_REQUEST); break; case PROVISION: processProvision(exchange); @@ -195,7 +194,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private void processRequest(CoapExchange exchange, SessionMsgType type) { + private void processRequest(CoapExchange exchange, CoapSessionMsgType type) { log.trace("Processing {}", exchange.advanced().getRequest()); deferAccept(exchange); Exchange advanced = exchange.advanced(); @@ -218,7 +217,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { } } - private void processAccessTokenRequest(CoapExchange exchange, SessionMsgType type, Request request) { + private void processAccessTokenRequest(CoapExchange exchange, CoapSessionMsgType type, Request request) { Optional credentials = decodeCredentials(request); if (credentials.isEmpty()) { exchange.respond(CoAP.ResponseCode.UNAUTHORIZED); @@ -228,7 +227,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { new CoapDeviceAuthCallback(exchange, (deviceCredentials, deviceProfile) -> processRequest(exchange, type, request, deviceCredentials, deviceProfile))); } - private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) { + private void processRequest(CoapExchange exchange, CoapSessionMsgType type, Request request, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) { TbCoapClientState clientState = null; try { clientState = clients.getOrCreateClient(type, deviceCredentials, deviceProfile); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java index 36ef8472de..a8531a7fba 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/CoapClientContext.java @@ -18,7 +18,7 @@ package org.thingsboard.server.transport.coap.client; import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.transport.coap.CoapSessionMsgType; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.gen.transport.TransportProtos; @@ -33,7 +33,7 @@ public interface CoapClientContext { AtomicInteger getNotificationCounterByToken(String token); - TbCoapClientState getOrCreateClient(SessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException; + TbCoapClientState getOrCreateClient(CoapSessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException; TransportProtos.SessionInfoProto getNewSyncSession(TbCoapClientState clientState); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index f70967b72d..26effe7058 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -45,7 +45,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.session.FeatureType; -import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.transport.coap.CoapSessionMsgType; import org.thingsboard.server.common.transport.DeviceDeletedEvent; import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent; import org.thingsboard.server.common.transport.DeviceUpdatedEvent; @@ -388,7 +388,7 @@ public class DefaultCoapClientContext implements CoapClientContext { } @Override - public TbCoapClientState getOrCreateClient(SessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException { + public TbCoapClientState getOrCreateClient(CoapSessionMsgType type, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) throws AdaptorException { DeviceId deviceId = deviceCredentials.getDeviceInfo().getDeviceId(); TbCoapClientState state = getClientState(deviceId); state.lock(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java index 302bd20c8b..eef9a53024 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java @@ -52,7 +52,7 @@ import java.util.stream.Collectors; public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { @Autowired - private TbLwM2MModelConfigStore modelStore; + TbLwM2MModelConfigStore modelStore; @Autowired @Lazy @@ -67,14 +67,14 @@ public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { @Autowired private LwM2MTelemetryLogService logService; - private ConcurrentMap currentModelConfigs; + ConcurrentMap currentModelConfigs; @AfterStartUp(order = AfterStartUp.BEFORE_TRANSPORT_SERVICE) - private void init() { + public void init() { List models = modelStore.getAll(); log.debug("Fetched model configs: {}", models); currentModelConfigs = models.stream() - .collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m)); + .collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m, (existing, replacement) -> existing)); } @Override diff --git a/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java new file mode 100644 index 0000000000..fc54ca9e0b --- /dev/null +++ b/common/transport/lwm2m/src/test/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImplTest.java @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MModelConfigStore; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +class LwM2MModelConfigServiceImplTest { + + LwM2MModelConfigServiceImpl service; + TbLwM2MModelConfigStore modelStore; + + @BeforeEach + void setUp() { + service = new LwM2MModelConfigServiceImpl(); + modelStore = mock(TbLwM2MModelConfigStore.class); + service.modelStore = modelStore; + } + + @Test + void testInitWithDuplicatedModels() { + LwM2MModelConfig config = new LwM2MModelConfig("urn:imei:951358811362976"); + List models = List.of(config, config); + willReturn(models).given(modelStore).getAll(); + service.init(); + assertThat(service.currentModelConfigs).containsExactlyEntriesOf(Map.of(config.getEndpoint(), config)); + } + + @Test + void testInitWithNonUniqueEndpoints() { + LwM2MModelConfig configAlfa = new LwM2MModelConfig("urn:imei:951358811362976"); + LwM2MModelConfig configBravo = new LwM2MModelConfig("urn:imei:151358811362976"); + LwM2MModelConfig configDelta = new LwM2MModelConfig("urn:imei:151358811362976"); + assertThat(configBravo.getEndpoint()).as("non-unique endpoints provided").isEqualTo(configDelta.getEndpoint()); + List models = List.of(configAlfa, configBravo, configDelta); + willReturn(models).given(modelStore).getAll(); + service.init(); + assertThat(service.currentModelConfigs).containsExactlyInAnyOrderEntriesOf(Map.of( + configAlfa.getEndpoint(), configAlfa, + configBravo.getEndpoint(), configBravo + )); + } + + @Test + void testInitWithEmptyModels() { + willReturn(Collections.emptyList()).given(modelStore).getAll(); + service.init(); + assertThat(service.currentModelConfigs).isEmpty(); + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 95603e751c..5cd6ba9145 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -71,6 +71,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; +import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; @@ -85,7 +86,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMess @Slf4j public abstract class AbstractGatewaySessionHandler { - protected static final String DEFAULT_DEVICE_TYPE = "default"; private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; private static final String DEVICE_PROPERTY = "device"; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 3b827edbae..497540ee44 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -49,14 +49,14 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.limit.LimitedApi; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; @@ -583,7 +583,7 @@ public class DefaultTransportService implements TransportService { metaData.putValue("deviceType", sessionInfo.getDeviceType()); metaData.putValue("ts", tsKv.getTs() + ""); JsonObject json = JsonUtils.getJsonObject(tsKv.getKvList()); - sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, SessionMsgType.POST_TELEMETRY_REQUEST, packCallback); + sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, TbMsgType.POST_TELEMETRY_REQUEST, packCallback); } } } @@ -603,7 +603,7 @@ public class DefaultTransportService implements TransportService { } metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "false"); CustomerId customerId = getCustomerId(sessionInfo); - sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, SessionMsgType.POST_ATTRIBUTES_REQUEST, + sendToRuleEngine(tenantId, deviceId, customerId, sessionInfo, json, metaData, TbMsgType.POST_ATTRIBUTES_REQUEST, new TransportTbQueueCallback(new ApiStatsProxyCallback<>(tenantId, customerId, msg.getKvList().size(), callback))); } } @@ -723,7 +723,7 @@ public class DefaultTransportService implements TransportService { metaData.putValue("serviceId", serviceInfoProvider.getServiceId()); metaData.putValue("sessionId", sessionId.toString()); sendToRuleEngine(tenantId, deviceId, getCustomerId(sessionInfo), sessionInfo, json, metaData, - SessionMsgType.TO_SERVER_RPC_REQUEST, new TransportTbQueueCallback(callback)); + TbMsgType.TO_SERVER_RPC_REQUEST, new TransportTbQueueCallback(callback)); String requestId = sessionId + "-" + msg.getRequestId(); toServerRpcPendingMap.put(requestId, new RpcRequestMetadata(sessionId, msg.getRequestId())); scheduler.schedule(() -> processTimeout(requestId), clientSideRpcTimeout, TimeUnit.MILLISECONDS); @@ -1136,7 +1136,7 @@ public class DefaultTransportService implements TransportService { } private void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, CustomerId customerId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, - TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { + TbMsgMetaData metaData, TbMsgType tbMsgType, TbQueueCallback callback) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); RuleChainId ruleChainId; @@ -1151,7 +1151,7 @@ public class DefaultTransportService implements TransportService { queueName = deviceProfile.getDefaultQueueName(); } - TbMsg tbMsg = TbMsg.newMsg(queueName, sessionMsgType.name(), deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); + TbMsg tbMsg = TbMsg.newMsg(queueName, tbMsgType, deviceId, customerId, metaData, gson.toJson(json), ruleChainId, null); sendToRuleEngine(tenantId, tbMsg, callback); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java new file mode 100644 index 0000000000..033aa4e0bc --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "device") +@Data +public class DeviceConnectivityConfiguration { + private Map connectivity; + + public boolean isEnabled(String protocol) { + var info = connectivity.get(protocol); + return info != null && info.isEnabled(); + } +} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java similarity index 73% rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java rename to dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java index 2535762ad5..b243be9995 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbRelationTypes.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityInfo.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.api; +package org.thingsboard.server.dao.device; -/** - * Created by ashvayka on 19.01.18. - */ -public final class TbRelationTypes { - - public static String SUCCESS = "Success"; - public static String FAILURE = "Failure"; +import lombok.Data; +@Data +public class DeviceConnectivityInfo { + private boolean enabled; + private String host; + private String port; + private String pemCertFile; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java new file mode 100644 index 0000000000..c06103d8f3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java @@ -0,0 +1,268 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.device; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ResourceUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.util.DeviceConnectivityUtil; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.CHECK_DOCUMENTATION; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT; +import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS; + +@Service("DeviceConnectivityDaoService") +@Slf4j +public class DeviceConnectivityServiceImpl implements DeviceConnectivityService { + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId "; + public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry"; + + @Autowired + private DeviceCredentialsService deviceCredentialsService; + + @Autowired + private DeviceProfileService deviceProfileService; + + @Autowired + private DeviceConnectivityConfiguration deviceConnectivityConfiguration; + + @Override + public JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException { + DeviceId deviceId = device.getId(); + log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId); + validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); + + DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId); + DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId()); + DeviceTransportType transportType = deviceProfile.getTransportType(); + + ObjectNode commands = JacksonUtil.newObjectNode(); + switch (transportType) { + case DEFAULT: + Optional.ofNullable(getHttpTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(HTTP, v)); + Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(MQTT, v)); + Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(COAP, v)); + break; + case MQTT: + MqttDeviceProfileTransportConfiguration transportConfiguration = + (MqttDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration(); + //TODO: add sparkplug command with emulator (check SSL) + if (transportConfiguration.isSparkplug()) { + ObjectNode sparkplug = JacksonUtil.newObjectNode(); + sparkplug.put("sparkplug", CHECK_DOCUMENTATION); + commands.set(MQTT, sparkplug); + } else { + String topicName = transportConfiguration.getDeviceTelemetryTopic(); + + Optional.ofNullable(getMqttTransportPublishCommands(baseUrl, topicName, creds)) + .ifPresent(v -> commands.set(MQTT, v)); + } + break; + case COAP: + Optional.ofNullable(getCoapTransportPublishCommands(baseUrl, creds)) + .ifPresent(v -> commands.set(COAP, v)); + break; + default: + commands.put(transportType.name(), CHECK_DOCUMENTATION); + } + return commands; + } + + @Override + public Resource getPemCertFile(String protocol) { + String certFilePath = deviceConnectivityConfiguration.getConnectivity() + .get(protocol) + .getPemCertFile(); + + if (StringUtils.isNotBlank(certFilePath) && ResourceUtils.resourceExists(this, certFilePath)) { + return new ClassPathResource(certFilePath); + } else { + return null; + } + } + + private JsonNode getHttpTransportPublishCommands(String defaultHostname, DeviceCredentials deviceCredentials) throws URISyntaxException { + ObjectNode httpCommands = JacksonUtil.newObjectNode(); + Optional.ofNullable(getHttpPublishCommand(HTTP, defaultHostname, deviceCredentials)) + .ifPresent(v -> httpCommands.put(HTTP, v)); + Optional.ofNullable(getHttpPublishCommand(HTTPS, defaultHostname, deviceCredentials)) + .ifPresent(v -> httpCommands.put(HTTPS, v)); + return httpCommands.isEmpty() ? null : httpCommands; + } + + private String getHttpPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol); + if (properties == null || !properties.isEnabled() || + deviceCredentials.getCredentialsType() != DeviceCredentialsType.ACCESS_TOKEN) { + return null; + } + String hostName = getHost(baseUrl, properties); + String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); + + return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials); + } + + private JsonNode getMqttTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + return getMqttTransportPublishCommands(baseUrl, DEFAULT_DEVICE_TELEMETRY_TOPIC, deviceCredentials); + } + + private JsonNode getMqttTransportPublishCommands(String baseUrl, String topic, DeviceCredentials deviceCredentials) throws URISyntaxException { + ObjectNode mqttCommands = JacksonUtil.newObjectNode(); + + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + mqttCommands.put(MQTTS, CHECK_DOCUMENTATION); + return mqttCommands; + } + + ObjectNode dockerMqttCommands = JacksonUtil.newObjectNode(); + + if (deviceConnectivityConfiguration.isEnabled(MQTT)) { + Optional.ofNullable(getMqttPublishCommand(baseUrl, topic, deviceCredentials)). + ifPresent(v -> mqttCommands.put(MQTT, v)); + + Optional.ofNullable(getDockerMqttPublishCommand(MQTT, baseUrl, topic, deviceCredentials)) + .ifPresent(v -> dockerMqttCommands.put(MQTT, v)); + } + + if (deviceConnectivityConfiguration.isEnabled(MQTTS)) { + List mqttsPublishCommand = getMqttsPublishCommand(baseUrl, topic, deviceCredentials); + if (mqttsPublishCommand != null) { + ArrayNode arrayNode = mqttCommands.putArray(MQTTS); + mqttsPublishCommand.forEach(arrayNode::add); + } + + Optional.ofNullable(getDockerMqttPublishCommand(MQTTS, baseUrl, topic, deviceCredentials)) + .ifPresent(v -> dockerMqttCommands.put(MQTTS, v)); + } + + if (!dockerMqttCommands.isEmpty()) { + mqttCommands.set(DOCKER, dockerMqttCommands); + } + return mqttCommands.isEmpty() ? null : mqttCommands; + } + + private String getMqttPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(MQTT); + String mqttHost = getHost(baseUrl, properties); + String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); + return DeviceConnectivityUtil.getMqttPublishCommand(MQTT, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); + } + + private List getMqttsPublishCommand(String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(MQTTS); + String mqttHost = getHost(baseUrl, properties); + String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); + String pubCommand = DeviceConnectivityUtil.getMqttPublishCommand(MQTTS, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); + + ArrayList commands = new ArrayList<>(); + if (pubCommand != null) { + commands.add(DeviceConnectivityUtil.getCurlPemCertCommand(baseUrl, MQTTS)); + commands.add(pubCommand); + return commands; + } + return null; + } + + private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol); + String mqttHost = getHost(baseUrl, properties); + String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort(); + return DeviceConnectivityUtil.getDockerMqttPublishCommand(protocol, baseUrl, mqttHost, mqttPort, deviceTelemetryTopic, deviceCredentials); + } + + private JsonNode getCoapTransportPublishCommands(String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + ObjectNode coapCommands = JacksonUtil.newObjectNode(); + + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { + coapCommands.put(COAPS, CHECK_DOCUMENTATION); + return coapCommands; + } + + ObjectNode dockerCoapCommands = JacksonUtil.newObjectNode(); + + if (deviceConnectivityConfiguration.isEnabled(COAP)) { + Optional.ofNullable(getCoapPublishCommand(COAP, baseUrl, deviceCredentials)) + .ifPresent(v -> coapCommands.put(COAP, v)); + + Optional.ofNullable(getDockerCoapPublishCommand(COAP, baseUrl, deviceCredentials)) + .ifPresent(v -> dockerCoapCommands.put(COAP, v)); + } + + if (deviceConnectivityConfiguration.isEnabled(COAPS)) { + Optional.ofNullable(getCoapPublishCommand(COAPS, baseUrl, deviceCredentials)) + .ifPresent(v -> coapCommands.put(COAPS, v)); + + Optional.ofNullable(getDockerCoapPublishCommand(COAPS, baseUrl, deviceCredentials)) + .ifPresent(v -> dockerCoapCommands.put(COAPS, v)); + } + + if (!dockerCoapCommands.isEmpty()) { + coapCommands.set(DOCKER, dockerCoapCommands); + } + + return coapCommands.isEmpty() ? null : coapCommands; + } + + private String getCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol); + String hostName = getHost(baseUrl, properties); + String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); + return DeviceConnectivityUtil.getCoapPublishCommand(protocol, hostName, port, deviceCredentials); + } + + private String getDockerCoapPublishCommand(String protocol, String baseUrl, DeviceCredentials deviceCredentials) throws URISyntaxException { + DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(protocol); + String host = getHost(baseUrl, properties); + String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort(); + return DeviceConnectivityUtil.getDockerCoapPublishCommand(protocol, host, port, deviceCredentials); + } + + private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException { + return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java index 425724bf2c..92289b7178 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java @@ -19,13 +19,13 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.springframework.beans.factory.annotation.Value; +import org.thingsboard.common.util.ThingsBoardExecutors; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; /** * Created by ashvayka on 21.02.17. @@ -34,9 +34,12 @@ public abstract class CassandraAbstractAsyncDao extends CassandraAbstractDao { protected ExecutorService readResultsProcessingExecutor; + @Value("${cassandra.query.result_processing_threads:50}") + private int threadPoolSize; + @PostConstruct public void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("cassandra-callback")); + readResultsProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "cassandra-callback"); } @PreDestroy diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index fb86e09ab3..f7ae4a1596 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -220,17 +220,36 @@ public class BaseRelationService implements RelationService { return future; } + @Transactional + @Override + public void deleteEntityCommonRelations(TenantId tenantId, EntityId entityId) { + deleteEntityRelations(tenantId, entityId, RelationTypeGroup.COMMON); + } + @Transactional @Override public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { + deleteEntityRelations(tenantId, entityId, null); + } + + @Transactional + public void deleteEntityRelations(TenantId tenantId, EntityId entityId, RelationTypeGroup relationTypeGroup) { log.trace("Executing deleteEntityRelations [{}]", entityId); validate(entityId); - List inboundRelations = new ArrayList<>(relationDao.findAllByTo(tenantId, entityId)); - List outboundRelations = new ArrayList<>(relationDao.findAllByFrom(tenantId, entityId)); + List inboundRelations = relationTypeGroup == null + ? relationDao.findAllByTo(tenantId, entityId) + : relationDao.findAllByTo(tenantId, entityId, relationTypeGroup); + List outboundRelations = relationTypeGroup == null + ? relationDao.findAllByFrom(tenantId, entityId) + : relationDao.findAllByFrom(tenantId, entityId, relationTypeGroup); if (!inboundRelations.isEmpty()) { try { - relationDao.deleteInboundRelations(tenantId, entityId); + if (relationTypeGroup == null) { + relationDao.deleteInboundRelations(tenantId, entityId); + } else { + relationDao.deleteInboundRelations(tenantId, entityId, relationTypeGroup); + } } catch (ConcurrencyFailureException e) { log.debug("Concurrency exception while deleting relations [{}]", inboundRelations, e); } @@ -241,7 +260,11 @@ public class BaseRelationService implements RelationService { } if (!outboundRelations.isEmpty()) { - relationDao.deleteOutboundRelations(tenantId, entityId); + if (relationTypeGroup == null) { + relationDao.deleteOutboundRelations(tenantId, entityId); + } else { + relationDao.deleteOutboundRelations(tenantId, entityId, relationTypeGroup); + } for (EntityRelation relation : outboundRelations) { eventPublisher.publishEvent(EntityRelationEvent.from(relation)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index 7fee4a31ff..250a0c6105 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -64,8 +64,12 @@ public interface RelationDao { void deleteOutboundRelations(TenantId tenantId, EntityId entity); + void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); + void deleteInboundRelations(TenantId tenantId, EntityId entity); + void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup); + ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity); List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index f35e7e608f..cf2ff1e7a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.event.RuleChainDebugEventFilter; import org.thingsboard.server.common.data.event.RuleNodeDebugEventFilter; import org.thingsboard.server.common.data.event.StatisticsEventFilter; import org.thingsboard.server.common.data.id.EventId; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.stats.StatsFactory; @@ -280,7 +281,7 @@ public class JpaBaseEventDao implements EventDao { private PageData findEventByFilter(UUID tenantId, UUID entityId, LifeCycleEventFilter eventFilter, TimePageLink pageLink) { boolean statusFilterEnabled = !StringUtils.isEmpty(eventFilter.getStatus()); - boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase("Success"); + boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase(TbNodeConnectionType.SUCCESS); return DaoUtil.toPageData( lcEventRepository.findEvents( tenantId, @@ -359,7 +360,7 @@ public class JpaBaseEventDao implements EventDao { private void removeEventsByFilter(UUID tenantId, UUID entityId, LifeCycleEventFilter eventFilter, Long startTime, Long endTime) { boolean statusFilterEnabled = !StringUtils.isEmpty(eventFilter.getStatus()); - boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase("Success"); + boolean statusFilter = statusFilterEnabled && eventFilter.getStatus().equalsIgnoreCase(TbNodeConnectionType.SUCCESS); lcEventRepository.removeEvents( tenantId, entityId, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index c1b17f160e..31125a0791 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -37,6 +37,7 @@ import org.thingsboard.server.dao.util.SqlDao; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -205,6 +206,15 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } } + @Override + public void deleteOutboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { + try { + relationRepository.deleteByFromIdAndFromTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name())); + } catch (ConcurrencyFailureException e) { + log.debug("Concurrency exception while deleting relations [{}]", entity, e); + } + } + @Override public void deleteInboundRelations(TenantId tenantId, EntityId entity) { try { @@ -214,6 +224,15 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple } } + @Override + public void deleteInboundRelations(TenantId tenantId, EntityId entity, RelationTypeGroup relationTypeGroup) { + try { + relationRepository.deleteByToIdAndToTypeAndRelationTypeGroupIn(entity.getId(), entity.getEntityType().name(), Collections.singletonList(relationTypeGroup.name())); + } catch (ConcurrencyFailureException e) { + log.debug("Concurrency exception while deleting relations [{}]", entity, e); + } + } + @Override public ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity) { return service.submit( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index a3d6d8570d..10c8c826eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -82,4 +82,9 @@ public interface RelationRepository @Query("DELETE FROM RelationEntity r where r.toId = :toId and r.toType = :toType and r.relationTypeGroup in :relationTypeGroups") void deleteByToIdAndToTypeAndRelationTypeGroupIn(@Param("toId") UUID toId, @Param("toType") String toType, @Param("relationTypeGroups") List relationTypeGroups); + @Transactional + @Modifying + @Query("DELETE FROM RelationEntity r where r.fromId = :fromId and r.fromType = :fromType and r.relationTypeGroup in :relationTypeGroups") + void deleteByFromIdAndFromTypeAndRelationTypeGroupIn(@Param("fromId") UUID fromId, @Param("fromType") String fromType, @Param("relationTypeGroups") List relationTypeGroups); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index 4c8d0c5afe..d6c3a8efdd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -121,7 +121,7 @@ public abstract class AbstractBufferedRateExecutor taskCtx = null; @@ -169,7 +169,7 @@ public abstract class AbstractBufferedRateExecutor= printQueriesFreq) { printQueriesIdx.set(0); String query = queryToString(finalTaskCtx); - log.info("[{}] Cassandra query: {}", taskCtx.getId(), query); + log.info("[{}][{}] Cassandra query: {}", getBufferName(), taskCtx.getId(), query); } } logTask("Processing", finalTaskCtx); @@ -222,7 +222,7 @@ public abstract class AbstractBufferedRateExecutor taskCtx) { @@ -298,7 +298,7 @@ public abstract class AbstractBufferedRateExecutor3.8.1 3.21.9 1.42.1 - 1.0.6 + 1.0.7 1.18.18 1.2.4 1.2.5 diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java index 5c54687205..22ffe34769 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EmptyNodeConfiguration.java @@ -24,7 +24,6 @@ public class EmptyNodeConfiguration implements NodeConfigurationDeprecated: This method is deprecated since version 3.5.2 and should only be used when you need to + * specify a custom message type that doesn't exist in the {@link TbMsgType} enum. For standard message types, + * it is recommended to use the {@link #newMsg(String, TbMsgType, EntityId, CustomerId, TbMsgMetaData, String)} + * method instead.

+ * + * @param queueName the name of the queue where the message will be sent + * @param type the type of the message + * @param originator the originator of the message + * @param customerId the ID of the customer associated with the message + * @param metaData the metadata of the message + * @param data the data of the message + * @return new TbMsg instance + */ + @Deprecated(since = "3.5.2") TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data); + @Deprecated(since = "3.5.2", forRemoval = true) TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data); + TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data); + + TbMsg newMsg(String queueName, TbMsgType type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data); + + TbMsg transformMsg(TbMsg origMsg, TbMsgType type, EntityId originator, TbMsgMetaData metaData, String data); + + TbMsg transformMsg(TbMsg origMsg, TbMsgMetaData metaData, String data); + + TbMsg transformMsgOriginator(TbMsg origMsg, EntityId originator); + TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId); TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId); TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId); - // TODO: Does this changes the message? + @Deprecated(since = "3.5.2", forRemoval = true) TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action); + TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, TbMsgType actionMsgType); + TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List attributes); TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List keys); diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java index 956cfaf40c..cb5514a82b 100644 --- a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -22,6 +22,7 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -43,7 +44,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.put("data_key", "data_value"); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value data_value", result); } @@ -57,7 +58,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.put("key", "data_value"); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals(pattern, result); } @@ -71,7 +72,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.put("key", "data_value"); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value data_value", result); } @@ -92,7 +93,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.set("key1", key1Node); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value value3", result); } @@ -113,7 +114,7 @@ public class TbNodeUtilsTest { ObjectNode node = JacksonUtil.newObjectNode(); node.set("key1", key1Node); - TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); String result = TbNodeUtils.processPattern(pattern, msg); Assert.assertEquals("ABC metadata_value $[key1.key2[0].key3]", result); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java index 3ca55f5db9..57871c91ff 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java @@ -26,6 +26,8 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -55,13 +57,13 @@ public abstract class TbAbstractAlarmNode { if (alarmResult.alarm == null) { - ctx.tellNext(msg, "False"); + ctx.tellNext(msg, TbNodeConnectionType.FALSE); } else if (alarmResult.isCreated) { - tellNext(ctx, msg, alarmResult, DataConstants.ENTITY_CREATED, "Created"); + tellNext(ctx, msg, alarmResult, TbMsgType.ENTITY_CREATED, "Created"); } else if (alarmResult.isUpdated) { - tellNext(ctx, msg, alarmResult, DataConstants.ENTITY_UPDATED, "Updated"); + tellNext(ctx, msg, alarmResult, TbMsgType.ENTITY_UPDATED, "Updated"); } else if (alarmResult.isCleared) { - tellNext(ctx, msg, alarmResult, DataConstants.ALARM_CLEAR, "Cleared"); + tellNext(ctx, msg, alarmResult, TbMsgType.ALARM_CLEAR, "Cleared"); } else { ctx.tellSuccess(msg); } @@ -71,13 +73,13 @@ public abstract class TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg); - protected ListenableFuture buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) { + protected ListenableFuture buildAlarmDetails(TbMsg msg, JsonNode previousDetails) { try { TbMsg dummyMsg = msg; if (previousDetails != null) { TbMsgMetaData metaData = msg.getMetaData().copy(); metaData.putValue(PREV_ALARM_DETAILS, JacksonUtil.toString(previousDetails)); - dummyMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msg.getData()); + dummyMsg = TbMsg.transformMsgMetadata(msg, metaData); } return scriptEngine.executeJsonAsync(dummyMsg); } catch (Exception e) { @@ -96,7 +98,7 @@ public abstract class TbAbstractAlarmNode ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), alarmAction), throwable -> ctx.tellFailure(toAlarmMsg(ctx, alarmResult, msg), throwable)); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 1ed7689bd6..ea53138d10 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -57,8 +57,8 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.FAILURE; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS; @Slf4j public abstract class TbAbstractRelationActionNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index a8d7d4985f..d0215a441f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -71,7 +71,7 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); - ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); + ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); return Futures.transform(asyncDetails, details -> { ctx.logJsEvalResponse(); AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index 6f7bb7cf8d..c103dd4006 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -36,7 +36,6 @@ import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import javax.annotation.Nullable; @@ -45,7 +44,12 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbNodeConnectionType.SUCCESS; @Slf4j @RuleNode( @@ -71,14 +75,11 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (DataConstants.ATTRIBUTES_UPDATED.equals(msg.getType()) || - DataConstants.ATTRIBUTES_DELETED.equals(msg.getType()) || - DataConstants.ACTIVITY_EVENT.equals(msg.getType()) || - DataConstants.INACTIVITY_EVENT.equals(msg.getType()) || - SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msg.getType())) { + if (msg.isTypeOneOf(ATTRIBUTES_UPDATED, ATTRIBUTES_DELETED, + ACTIVITY_EVENT, INACTIVITY_EVENT, POST_ATTRIBUTES_REQUEST)) { if (!msg.getMetaData().getData().isEmpty()) { long now = System.currentTimeMillis(); - String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ? + String scope = msg.isTypeOf(POST_ATTRIBUTES_REQUEST) ? DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue(DataConstants.SCOPE); ListenableFuture> entityViewsFuture = @@ -90,9 +91,9 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { long startTime = entityView.getStartTimeMs(); long endTime = entityView.getEndTimeMs(); if ((endTime != 0 && endTime > now && startTime < now) || (endTime == 0 && startTime < now)) { - if (DataConstants.ATTRIBUTES_DELETED.equals(msg.getType())) { + if (msg.isTypeOf(ATTRIBUTES_DELETED)) { List attributes = new ArrayList<>(); - for (JsonElement element : new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray()) { + for (JsonElement element : JsonParser.parseString(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray()) { if (element.isJsonPrimitive()) { JsonPrimitive value = element.getAsJsonPrimitive(); if (value.isString()) { @@ -107,7 +108,7 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { getFutureCallback(ctx, msg, entityView)); } } else { - Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); List filteredAttributes = attributes.stream().filter(attr -> attributeContainsInEntityView(scope, attr.getKey(), entityView)).collect(Collectors.toList()); ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), entityView.getId(), scope, filteredAttributes, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index 84e82b76e8..b08fd1a562 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -120,7 +120,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode future = createRelationIfAbsent(ctx, msg, entity, relationType); return Futures.transform(future, result -> { if (result && config.isChangeOriginatorToRelatedEntity()) { - TbMsg tbMsg = ctx.transformMsg(msg, msg.getType(), entity.getEntityId(), msg.getMetaData(), msg.getData()); + TbMsg tbMsg = ctx.transformMsgOriginator(msg, entity.getEntityId()); return new RelationContainer(tbMsg, result); } return new RelationContainer(msg, result); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 88f2be46a8..021123a308 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -24,17 +24,16 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, @@ -48,8 +47,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; ) public class TbMsgCountNode implements TbNode { - private static final String TB_MSG_COUNT_NODE_MSG = "TbMsgCountNodeMsg"; - private AtomicLong messagesProcessed = new AtomicLong(0); private final Gson gson = new Gson(); private UUID nextTickId; @@ -68,7 +65,7 @@ public class TbMsgCountNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (msg.getType().equals(TB_MSG_COUNT_NODE_MSG) && msg.getId().equals(nextTickId)) { + if (msg.isTypeOf(TbMsgType.MSG_COUNT_SELF_MSG) && msg.getId().equals(nextTickId)) { JsonObject telemetryJson = new JsonObject(); telemetryJson.addProperty(this.telemetryPrefix + "_" + ctx.getServiceId(), messagesProcessed.longValue()); @@ -77,8 +74,8 @@ public class TbMsgCountNode implements TbNode { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("delta", Long.toString(System.currentTimeMillis() - lastScheduledTs + delay)); - TbMsg tbMsg = TbMsg.newMsg(msg.getQueueName(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), ctx.getTenantId(), msg.getCustomerId(), metaData, gson.toJson(telemetryJson)); - ctx.enqueueForTellNext(tbMsg, SUCCESS); + TbMsg tbMsg = TbMsg.newMsg(msg.getQueueName(), TbMsgType.POST_TELEMETRY_REQUEST, ctx.getTenantId(), msg.getCustomerId(), metaData, gson.toJson(telemetryJson)); + ctx.enqueueForTellNext(tbMsg, TbNodeConnectionType.SUCCESS); scheduleTickMsg(ctx, tbMsg); } else { messagesProcessed.incrementAndGet(); @@ -93,7 +90,7 @@ public class TbMsgCountNode implements TbNode { } lastScheduledTs = lastScheduledTs + delay; long curDelay = Math.max(0L, (lastScheduledTs - curTs)); - TbMsg tickMsg = ctx.newMsg(null, TB_MSG_COUNT_NODE_MSG, ctx.getSelfId(), msg != null ? msg.getCustomerId() : null, new TbMsgMetaData(), ""); + TbMsg tickMsg = ctx.newMsg(null, TbMsgType.MSG_COUNT_SELF_MSG, ctx.getSelfId(), msg != null ? msg.getCustomerId() : null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); nextTickId = tickMsg.getId(); ctx.tellSelf(tickMsg, curDelay); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java index a8a1137a98..e24c73f9d3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java @@ -29,7 +29,6 @@ import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -106,13 +105,13 @@ public class TbSnsNode extends TbAbstractExternalNode { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, result.getMessageId()); metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java index 2b9f8916cf..34072fbb8a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java @@ -28,7 +28,6 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -93,14 +92,14 @@ public class TbSqsNode extends TbAbstractExternalNode { var tbMsg = ackIfNeeded(ctx, msg); withCallback(publishMessageAsync(ctx, tbMsg), m -> tellSuccess(ctx, m), - t -> tellFailure(ctx, processException(ctx, tbMsg, t), t)); + t -> tellFailure(ctx, processException(tbMsg, t), t)); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { - return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg)); + return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg)); } - private TbMsg publishMessage(TbContext ctx, TbMsg msg) { + private TbMsg publishMessage(TbMsg msg) { String queueUrl = TbNodeUtils.processPattern(this.config.getQueueUrlPattern(), msg); SendMessageRequest sendMsgRequest = new SendMessageRequest(); sendMsgRequest.withQueueUrl(queueUrl); @@ -119,10 +118,10 @@ public class TbSqsNode extends TbAbstractExternalNode { sendMsgRequest.withMessageGroupId(msg.getOriginator().toString()); } SendMessageResult result = this.sqsClient.sendMessage(sendMsgRequest); - return processSendMessageResult(ctx, msg, result); + return processSendMessageResult(msg, result); } - private TbMsg processSendMessageResult(TbContext ctx, TbMsg origMsg, SendMessageResult result) { + private TbMsg processSendMessageResult(TbMsg origMsg, SendMessageResult result) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, result.getMessageId()); metaData.putValue(REQUEST_ID, result.getSdkResponseMetadata().getRequestId()); @@ -135,13 +134,13 @@ public class TbSqsNode extends TbAbstractExternalNode { if (!StringUtils.isEmpty(result.getSequenceNumber())) { metaData.putValue(SEQUENCE_NUMBER, result.getSequenceNumber()); } - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 51effe02ef..b31c98bc0f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -28,8 +28,11 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; @@ -41,7 +44,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @Slf4j @RuleNode( @@ -58,8 +60,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; public class TbMsgGeneratorNode implements TbNode { - private static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg"; - private TbMsgGeneratorNodeConfiguration config; private ScriptEngine scriptEngine; private long delay; @@ -97,7 +97,7 @@ public class TbMsgGeneratorNode implements TbNode { if (initialized.compareAndSet(false, true)) { this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); - scheduleTickMsg(ctx); + scheduleTickMsg(ctx, null); } } else if (initialized.compareAndSet(true, false)) { destroy(); @@ -107,14 +107,14 @@ public class TbMsgGeneratorNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { log.trace("onMsg, config {}, msg {}", config, msg); - if (initialized.get() && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) { + if (initialized.get() && msg.isTypeOf(TbMsgType.GENERATOR_NODE_SELF_MSG) && msg.getId().equals(nextTickId)) { TbStopWatch sw = TbStopWatch.create(); withCallback(generate(ctx, msg), m -> { log.trace("onMsg onSuccess callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg); if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { - ctx.enqueueForTellNext(m, SUCCESS); - scheduleTickMsg(ctx); + ctx.enqueueForTellNext(m, TbNodeConnectionType.SUCCESS); + scheduleTickMsg(ctx, msg); currentMsgCount++; } }, @@ -122,14 +122,14 @@ public class TbMsgGeneratorNode implements TbNode { log.trace("onMsg onFailure callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg, t); if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { ctx.tellFailure(msg, t); - scheduleTickMsg(ctx); + scheduleTickMsg(ctx, msg); currentMsgCount++; } }); } } - private void scheduleTickMsg(TbContext ctx) { + private void scheduleTickMsg(TbContext ctx, TbMsg msg) { log.trace("scheduleTickMsg, config {}", config); long curTs = System.currentTimeMillis(); if (lastScheduledTs == 0L) { @@ -137,7 +137,8 @@ public class TbMsgGeneratorNode implements TbNode { } lastScheduledTs = lastScheduledTs + delay; long curDelay = Math.max(0L, (lastScheduledTs - curTs)); - TbMsg tickMsg = ctx.newMsg(config.getQueueName(), TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), ""); + TbMsg tickMsg = ctx.newMsg(config.getQueueName(), TbMsgType.GENERATOR_NODE_SELF_MSG, ctx.getSelfId(), + getCustomerIdFromMsg(msg), TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); nextTickId = tickMsg.getId(); ctx.tellSelf(tickMsg, curDelay); } @@ -145,7 +146,7 @@ public class TbMsgGeneratorNode implements TbNode { private ListenableFuture generate(TbContext ctx, TbMsg msg) { log.trace("generate, config {}", config); if (prevMsg == null) { - prevMsg = ctx.newMsg(config.getQueueName(), "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); + prevMsg = ctx.newMsg(config.getQueueName(), TbMsg.EMPTY_STRING, originatorId, msg.getCustomerId(), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); } if (initialized.get()) { ctx.logJsEvalRequest(); @@ -160,6 +161,10 @@ public class TbMsgGeneratorNode implements TbNode { } + private CustomerId getCustomerIdFromMsg(TbMsg msg) { + return msg != null ? msg.getCustomerId() : null; + } + @Override public void destroy() { log.trace("destroy, config {}", config); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index 6890aa6bf4..81bb3d6772 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -24,9 +24,10 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; @@ -60,10 +61,7 @@ import java.util.concurrent.TimeUnit; @Slf4j public class TbMsgDeduplicationNode implements TbNode { - private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; public static final int TB_MSG_DEDUPLICATION_RETRY_DELAY = 10; - private static final String EMPTY_DATA = ""; - private static final TbMsgMetaData EMPTY_META_DATA = new TbMsgMetaData(); private TbMsgDeduplicationNodeConfiguration config; @@ -82,7 +80,7 @@ public class TbMsgDeduplicationNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { - if (TB_MSG_DEDUPLICATION_TIMEOUT_MSG.equals(msg.getType())) { + if (msg.isTypeOf(TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG)) { processDeduplication(ctx, msg.getOriginator()); } else { processOnRegularMsg(ctx, msg); @@ -196,7 +194,7 @@ public class TbMsgDeduplicationNode implements TbNode { private void enqueueForTellNextWithRetry(TbContext ctx, TbMsg msg, int retryAttempt) { if (config.getMaxRetries() > retryAttempt) { - ctx.enqueueForTellNext(msg, TbRelationTypes.SUCCESS, + ctx.enqueueForTellNext(msg, TbNodeConnectionType.SUCCESS, () -> { log.trace("[{}][{}][{}] Successfully enqueue deduplication result message!", ctx.getSelfId(), msg.getOriginator(), retryAttempt); }, @@ -210,7 +208,7 @@ public class TbMsgDeduplicationNode implements TbNode { } private void scheduleTickMsg(TbContext ctx, EntityId deduplicationId) { - ctx.tellSelf(ctx.newMsg(null, TB_MSG_DEDUPLICATION_TIMEOUT_MSG, deduplicationId, EMPTY_META_DATA, EMPTY_DATA), deduplicationInterval + 1); + ctx.tellSelf(ctx.newMsg(null, TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG, deduplicationId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING), deduplicationInterval + 1); } private String getMergedData(List msgs) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 1f18aea86e..5414cc2bbe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -23,6 +23,8 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -32,8 +34,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; - @Slf4j @RuleNode( type = ComponentType.ACTION, @@ -50,8 +50,6 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; ) public class TbMsgDelayNode implements TbNode { - private static final String TB_MSG_DELAY_NODE_MSG = "TbMsgDelayNodeMsg"; - private TbMsgDelayNodeConfiguration config; private Map pendingMsgs; @@ -63,7 +61,7 @@ public class TbMsgDelayNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (msg.getType().equals(TB_MSG_DELAY_NODE_MSG)) { + if (msg.isTypeOf(TbMsgType.DELAY_TIMEOUT_SELF_MSG)) { TbMsg pendingMsg = pendingMsgs.remove(UUID.fromString(msg.getData())); if (pendingMsg != null) { ctx.enqueueForTellNext( @@ -75,13 +73,13 @@ public class TbMsgDelayNode implements TbNode { pendingMsg.getMetaData(), pendingMsg.getData() ), - SUCCESS + TbNodeConnectionType.SUCCESS ); } } else { if (pendingMsgs.size() < config.getMaxPendingMsgs()) { pendingMsgs.put(msg.getId(), msg); - TbMsg tickMsg = ctx.newMsg(null, TB_MSG_DELAY_NODE_MSG, ctx.getSelfId(), msg.getCustomerId(), new TbMsgMetaData(), msg.getId().toString()); + TbMsg tickMsg = ctx.newMsg(null, TbMsgType.DELAY_TIMEOUT_SELF_MSG, ctx.getSelfId(), msg.getCustomerId(), TbMsgMetaData.EMPTY, msg.getId().toString()); ctx.tellSelf(tickMsg, getDelay(msg)); ctx.ack(msg); } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 318b38aa39..7888922dc6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -30,13 +30,23 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.DISCONNECT_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.TIMESERIES_UPDATED; + @Slf4j public abstract class AbstractTbMsgPushNode implements TbNode { @@ -57,7 +67,7 @@ public abstract class AbstractTbMsgPushNode metadata = msg.getMetaData().getData(); - EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msgType, metadata); + EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msg); Map entityBody = new HashMap<>(); JsonNode dataJson = JacksonUtil.toJsonNode(msg.getData()); switch (actionType) { @@ -148,45 +157,31 @@ public abstract class AbstractTbMsgPushNode metadata) { + protected EdgeEventActionType getEdgeEventActionTypeByMsgType(TbMsg msg) { EdgeEventActionType actionType; - if (SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) - || DataConstants.TIMESERIES_UPDATED.equals(msgType)) { + if (msg.isTypeOneOf(POST_TELEMETRY_REQUEST, TIMESERIES_UPDATED)) { actionType = EdgeEventActionType.TIMESERIES_UPDATED; - } else if (DataConstants.ATTRIBUTES_UPDATED.equals(msgType)) { + } else if (msg.isTypeOf(ATTRIBUTES_UPDATED)) { actionType = EdgeEventActionType.ATTRIBUTES_UPDATED; - } else if (SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType)) { + } else if (msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { actionType = EdgeEventActionType.POST_ATTRIBUTES; - } else if (DataConstants.ATTRIBUTES_DELETED.equals(msgType)) { + } else if (msg.isTypeOf(ATTRIBUTES_DELETED)) { actionType = EdgeEventActionType.ATTRIBUTES_DELETED; - } else if (DataConstants.CONNECT_EVENT.equals(msgType) - || DataConstants.DISCONNECT_EVENT.equals(msgType) - || DataConstants.ACTIVITY_EVENT.equals(msgType) - || DataConstants.INACTIVITY_EVENT.equals(msgType)) { - String scope = metadata.get(SCOPE); - if ( StringUtils.isEmpty(scope)) { - actionType = EdgeEventActionType.TIMESERIES_UPDATED; - } else { - actionType = EdgeEventActionType.ATTRIBUTES_UPDATED; - } + } else if (msg.isTypeOneOf(CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT)) { + String scope = msg.getMetaData().getValue(SCOPE); + actionType = StringUtils.isEmpty(scope) ? + EdgeEventActionType.TIMESERIES_UPDATED : EdgeEventActionType.ATTRIBUTES_UPDATED; } else { - log.warn("Unsupported msg type [{}]", msgType); - throw new IllegalArgumentException("Unsupported msg type: " + msgType); + String type = msg.getType(); + log.warn("Unsupported msg type [{}]", type); + throw new IllegalArgumentException("Unsupported msg type: " + type); } return actionType; } - protected boolean isSupportedMsgType(String msgType) { - return SessionMsgType.POST_TELEMETRY_REQUEST.name().equals(msgType) - || SessionMsgType.POST_ATTRIBUTES_REQUEST.name().equals(msgType) - || DataConstants.ATTRIBUTES_UPDATED.equals(msgType) - || DataConstants.ATTRIBUTES_DELETED.equals(msgType) - || DataConstants.TIMESERIES_UPDATED.equals(msgType) - || DataConstants.ALARM.equals(msgType) - || DataConstants.CONNECT_EVENT.equals(msgType) - || DataConstants.DISCONNECT_EVENT.equals(msgType) - || DataConstants.ACTIVITY_EVENT.equals(msgType) - || DataConstants.INACTIVITY_EVENT.equals(msgType); + protected boolean isSupportedMsgType(TbMsg msg) { + return msg.isTypeOneOf(POST_TELEMETRY_REQUEST, POST_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATED, + ATTRIBUTES_DELETED, TIMESERIES_UPDATED, ALARM, CONNECT_EVENT, DISCONNECT_EVENT, ACTIVITY_EVENT, INACTIVITY_EVENT); } protected boolean isSupportedOriginator(EntityType entityType) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java index 834dbe2390..1fb000d709 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.external; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; public abstract class TbAbstractExternalNode implements TbNode { @@ -30,7 +30,7 @@ public abstract class TbAbstractExternalNode implements TbNode { protected void tellSuccess(TbContext ctx, TbMsg tbMsg) { if (forceAck) { - ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbRelationTypes.SUCCESS); + ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.SUCCESS); } else { ctx.tellSuccess(tbMsg); } @@ -39,13 +39,13 @@ public abstract class TbAbstractExternalNode implements TbNode { protected void tellFailure(TbContext ctx, TbMsg tbMsg, Throwable t) { if (forceAck) { if (t == null) { - ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbRelationTypes.FAILURE); + ctx.enqueueForTellNext(tbMsg.copyWithNewCtx(), TbNodeConnectionType.FAILURE); } else { ctx.enqueueForTellFailure(tbMsg.copyWithNewCtx(), t); } } else { if (t == null) { - ctx.tellNext(tbMsg, TbRelationTypes.FAILURE); + ctx.tellNext(tbMsg, TbNodeConnectionType.FAILURE); } else { ctx.tellFailure(tbMsg, t); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java index 21e109e8f3..39c37f5c81 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNode.java @@ -31,10 +31,11 @@ import org.thingsboard.server.common.data.plugin.ComponentType; type = ComponentType.FILTER, name = "asset profile switch", customRelations = true, - relationTypes = {}, + relationTypes = {"default"}, configClazz = EmptyNodeConfiguration.class, nodeDescription = "Route incoming messages based on the name of the asset profile", - nodeDetails = "Route incoming messages based on the name of the asset profile. The asset profile name is case-sensitive", + nodeDetails = "Route incoming messages based on the name of the asset profile. The asset profile name is case-sensitive.

" + + "Output connections: Asset profile name or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode { @@ -42,7 +43,8 @@ public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override protected String getRelationType(TbContext ctx, EntityId originator) throws TbNodeException { if (!EntityType.ASSET.equals(originator.getEntityType())) { - throw new TbNodeException("Unsupported originator type: " + originator.getEntityType() + "! Only 'ASSET' type is allowed."); + throw new TbNodeException("Unsupported originator type: " + originator.getEntityType().getNormalName() + "!" + + " Only " + EntityType.ASSET.getNormalName() + " type is allowed."); } AssetProfile assetProfile = ctx.getAssetProfileCache().get(ctx.getTenantId(), (AssetId) originator); if (assetProfile == null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java index dcaf6698cd..ca58e47554 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.filter; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -28,24 +27,25 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import javax.annotation.Nullable; -import java.io.IOException; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "check alarm status", + name = "alarm status filter", configClazz = TbCheckAlarmStatusNodeConfig.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Checks alarm status.", - nodeDetails = "Checks the alarm status to match one of the specified statuses.", + nodeDetails = "Checks the alarm status to match one of the specified statuses.

" + + "Output connections: True, False, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckAlarmStatusConfig") public class TbCheckAlarmStatusNode implements TbNode { + private TbCheckAlarmStatusNodeConfig config; @Override @@ -60,33 +60,24 @@ public class TbCheckAlarmStatusNode implements TbNode { ListenableFuture latest = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId()); - Futures.addCallback(latest, new FutureCallback() { + Futures.addCallback(latest, new FutureCallback<>() { @Override public void onSuccess(@Nullable Alarm result) { - if (result != null) { - boolean isPresent = false; - for (AlarmStatus alarmStatus : config.getAlarmStatusList()) { - if (result.getStatus() == alarmStatus) { - isPresent = true; - break; - } - } - if (isPresent) { - ctx.tellNext(msg, "True"); - } else { - ctx.tellNext(msg, "False"); - } - } else { + if (result == null) { ctx.tellFailure(msg, new TbNodeException("No such alarm found.")); + return; } + boolean isPresent = config.getAlarmStatusList().stream() + .anyMatch(alarmStatus -> result.getStatus() == alarmStatus); + ctx.tellNext(msg, isPresent ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } @Override public void onFailure(Throwable t) { ctx.tellFailure(msg, t); } - }, MoreExecutors.directExecutor()); - } catch (IllegalArgumentException e) { + }, ctx.getDbCallbackExecutor()); + } catch (Exception e) { log.error("Failed to parse alarm: [{}]", msg.getData()); throw new TbNodeException(e); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java index d979eb10ca..4c15d635b8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeConfig.java @@ -24,12 +24,14 @@ import java.util.List; @Data public class TbCheckAlarmStatusNodeConfig implements NodeConfiguration { + private List alarmStatusList; @Override public TbCheckAlarmStatusNodeConfig defaultConfiguration() { - TbCheckAlarmStatusNodeConfig config = new TbCheckAlarmStatusNodeConfig(); + var config = new TbCheckAlarmStatusNodeConfig(); config.setAlarmStatusList(Arrays.asList(AlarmStatus.ACTIVE_ACK, AlarmStatus.ACTIVE_UNACK)); return config; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java index 3853b3a62a..a5ee3b969b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -33,12 +34,12 @@ import java.util.Map; @RuleNode( type = ComponentType.FILTER, name = "check fields presence", - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, configClazz = TbCheckMessageNodeConfiguration.class, nodeDescription = "Checks the presence of the specified fields in the message and/or metadata.", - nodeDetails = "Checks the presence of the specified fields in the message and/or metadata. " + - "By default, the rule node checks that all specified fields need to be present. " + - "Uncheck the 'Check that all specified fields are present' if the presence of at least one field is sufficient.", + nodeDetails = "By default, the rule node checks that all specified fields are present. " + + "Uncheck the 'Check that all selected fields are present' if the presence of at least one field is sufficient.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckMessageConfig") public class TbCheckMessageNode implements TbNode { @@ -59,11 +60,10 @@ public class TbCheckMessageNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { try { - if (config.isCheckAllKeys()) { - ctx.tellNext(msg, allKeysData(msg) && allKeysMetadata(msg) ? "True" : "False"); - } else { - ctx.tellNext(msg, atLeastOneData(msg) || atLeastOneMetadata(msg) ? "True" : "False"); - } + String relationType = config.isCheckAllKeys() ? + allKeysData(msg) && allKeysMetadata(msg) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE : + atLeastOneData(msg) || atLeastOneMetadata(msg) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE; + ctx.tellNext(msg, relationType); } catch (Exception e) { ctx.tellFailure(msg, e); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java index f145be3fd2..25a6efa417 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeConfiguration.java @@ -22,7 +22,7 @@ import java.util.Collections; import java.util.List; @Data -public class TbCheckMessageNodeConfiguration implements NodeConfiguration { +public class TbCheckMessageNodeConfiguration implements NodeConfiguration { private List messageNames; private List metadataNames; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index 4a0d6b05e7..3af5c9bfe1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -15,22 +15,26 @@ */ package org.thingsboard.rule.engine.filter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbVersionedNode; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import java.util.List; @@ -43,15 +47,20 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "check relation", + name = "check relation presence", configClazz = TbCheckRelationNodeConfiguration.class, - relationTypes = {"True", "False"}, + version = 1, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Checks the presence of the relation between the originator of the message and other entities.", - nodeDetails = "If 'check relation to specific entity' is selected, one must specify a related entity. " + - "Otherwise, the rule node checks the presence of a relation to any entity that matches the direction and relation type criteria.", + nodeDetails = "If 'check relation to specific entity' is selected, you should specify a related entity. " + + "Otherwise, the rule node checks the presence of a relation to any entity. " + + "In both cases, relation lookup is based on configured direction and type.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeCheckRelationConfig") -public class TbCheckRelationNode implements TbNode { +public class TbCheckRelationNode implements TbVersionedNode { + + private static final String DIRECTION_PROPERTY_NAME = "direction"; private TbCheckRelationNodeConfiguration config; private EntityId singleEntityId; @@ -60,6 +69,9 @@ public class TbCheckRelationNode implements TbNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbCheckRelationNodeConfiguration.class); if (config.isCheckForSingleEntity()) { + if (StringUtils.isEmpty(config.getEntityType()) || StringUtils.isEmpty(config.getEntityId())) { + throw new TbNodeException("Entity should be specified!"); + } this.singleEntityId = EntityIdFactory.getByTypeAndId(config.getEntityType(), config.getEntityId()); ctx.checkTenantEntity(singleEntityId); } @@ -67,44 +79,56 @@ public class TbCheckRelationNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { - ListenableFuture checkRelationFuture; - if (config.isCheckForSingleEntity()) { - checkRelationFuture = processSingle(ctx, msg); - } else { - checkRelationFuture = processList(ctx, msg); - } - withCallback(checkRelationFuture, filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); + ListenableFuture checkRelationFuture = config.isCheckForSingleEntity() ? + processSingle(ctx, msg) : processList(ctx, msg); + withCallback(checkRelationFuture, + filterResult -> ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE), + t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } private ListenableFuture processSingle(TbContext ctx, TbMsg msg) { EntityId from; EntityId to; if (EntitySearchDirection.FROM.name().equals(config.getDirection())) { - from = singleEntityId; - to = msg.getOriginator(); - } else { to = singleEntityId; from = msg.getOriginator(); + } else { + from = singleEntityId; + to = msg.getOriginator(); } return ctx.getRelationService().checkRelationAsync(ctx.getTenantId(), from, to, config.getRelationType(), RelationTypeGroup.COMMON); } private ListenableFuture processList(TbContext ctx, TbMsg msg) { - if (EntitySearchDirection.FROM.name().equals(config.getDirection())) { - return Futures.transformAsync(ctx.getRelationService() - .findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON), this::isEmptyList, MoreExecutors.directExecutor()); - } else { - return Futures.transformAsync(ctx.getRelationService() - .findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON), this::isEmptyList, MoreExecutors.directExecutor()); - } + ListenableFuture> relationListFuture = EntitySearchDirection.FROM.name().equals(config.getDirection()) ? + ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON) : + ctx.getRelationService().findByToAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), config.getRelationType(), RelationTypeGroup.COMMON); + return Futures.transformAsync(relationListFuture, this::isEmptyList, ctx.getDbCallbackExecutor()); } private ListenableFuture isEmptyList(List entityRelations) { - if (entityRelations.isEmpty()) { - return Futures.immediateFuture(false); - } else { - return Futures.immediateFuture(true); + return entityRelations.isEmpty() ? Futures.immediateFuture(false) : Futures.immediateFuture(true); + } + + @Override + public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { + if (fromVersion == 0) { + var newConfigObjectNode = (ObjectNode) oldConfiguration; + if (!newConfigObjectNode.has(DIRECTION_PROPERTY_NAME)) { + throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' doesn't exists in configuration!"); + } + String direction = newConfigObjectNode.get(DIRECTION_PROPERTY_NAME).asText(); + if (EntitySearchDirection.TO.name().equals(direction)) { + newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.FROM.name()); + return new TbPair<>(true, newConfigObjectNode); + } + if (EntitySearchDirection.FROM.name().equals(direction)) { + newConfigObjectNode.put(DIRECTION_PROPERTY_NAME, EntitySearchDirection.TO.name()); + return new TbPair<>(true, newConfigObjectNode); + } + throw new TbNodeException("property to update: '" + DIRECTION_PROPERTY_NAME + "' has invalid value!"); } + return new TbPair<>(false, oldConfiguration); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java index aeda247583..1a8ab7068d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.filter; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; /** @@ -33,9 +34,9 @@ public class TbCheckRelationNodeConfiguration implements NodeConfigurationDevice profile name
or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode { @@ -42,7 +43,8 @@ public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override protected String getRelationType(TbContext ctx, EntityId originator) throws TbNodeException { if (!EntityType.DEVICE.equals(originator.getEntityType())) { - throw new TbNodeException("Unsupported originator type: " + originator.getEntityType() + "! Only 'DEVICE' type is allowed."); + throw new TbNodeException("Unsupported originator type: " + originator.getEntityType().getNormalName() + + "! Only " + EntityType.DEVICE.getNormalName() + " type is allowed."); } DeviceProfile deviceProfile = ctx.getDeviceProfileCache().get(ctx.getTenantId(), (DeviceId) originator); if (deviceProfile == null) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index 0bde9402a7..bcfaf78a65 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.script.ScriptLanguage; @@ -32,7 +33,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "script", relationTypes = {"True", "False"}, + name = "script", + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, configClazz = TbJsFilterNodeConfiguration.class, nodeDescription = "Filter incoming messages using TBEL or JS script", nodeDetails = "Evaluates boolean function using incoming message. " + @@ -40,7 +42,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Script function should return boolean value and accepts three parameters:
" + "Message payload can be accessed via msg property. For example msg.temperature < 10;
" + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + - "Message type can be accessed via msgType property.", + "Message type can be accessed via msgType property.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeScriptConfig" ) @@ -62,7 +65,7 @@ public class TbJsFilterNode implements TbNode { withCallback(scriptEngine.executeFilterAsync(msg), filterResult -> { ctx.logJsEvalResponse(); - ctx.tellNext(msg, filterResult ? "True" : "False"); + ctx.tellNext(msg, filterResult ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); }, t -> { ctx.tellFailure(msg, t); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index 706978846a..f55a172c02 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -44,7 +44,8 @@ import java.util.Set; "If Array is empty - message not routed to next Node. " + "Message payload can be accessed via msg property. For example msg.temperature < 10;
" + "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
" + - "Message type can be accessed via msgType property.", + "Message type can be accessed via msgType property.

" + + "Output connections: Custom connection(s) defined by switch node or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeSwitchConfig") public class TbJsSwitchNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 765deb2ea7..ca8ba9d561 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -31,11 +32,12 @@ import org.thingsboard.server.common.msg.TbMsg; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "message type", + name = "message type filter", configClazz = TbMsgTypeFilterNodeConfiguration.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Filter incoming messages by Message Type", - nodeDetails = "If incoming MessageType is expected - send Message via True chain, otherwise False chain is used.", + nodeDetails = "If incoming message type is expected - send Message via True chain, otherwise False chain is used.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeMessageTypeConfig") public class TbMsgTypeFilterNode implements TbNode { @@ -49,7 +51,7 @@ public class TbMsgTypeFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? "True" : "False"); + ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java index fb326a6d50..1c0b080653 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java @@ -17,11 +17,14 @@ package org.thingsboard.rule.engine.filter; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.Arrays; import java.util.List; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.TO_SERVER_RPC_REQUEST; + /** * Created by ashvayka on 19.01.18. */ @@ -32,11 +35,11 @@ public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration etc. via corresponding chain, otherwise Other chain is used.", + nodeDetails = "Sends messages with message types \"Post attributes\", \"Post telemetry\", \"RPC Request\"" + + " etc. via corresponding chain, otherwise Other chain is used.

" + + "Output connections: Message type connection, Other - if message type is custom or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbMsgTypeSwitchNode implements TbNode { @@ -52,83 +50,7 @@ public class TbMsgTypeSwitchNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - String relationType; - if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { - relationType = "Post attributes"; - } else if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { - relationType = "Post telemetry"; - } else if (msg.getType().equals(SessionMsgType.TO_SERVER_RPC_REQUEST.name())) { - relationType = "RPC Request from Device"; - } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT)) { - relationType = "Activity Event"; - } else if (msg.getType().equals(DataConstants.INACTIVITY_EVENT)) { - relationType = "Inactivity Event"; - } else if (msg.getType().equals(DataConstants.CONNECT_EVENT)) { - relationType = "Connect Event"; - } else if (msg.getType().equals(DataConstants.DISCONNECT_EVENT)) { - relationType = "Disconnect Event"; - } else if (msg.getType().equals(DataConstants.ENTITY_CREATED)) { - relationType = "Entity Created"; - } else if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) { - relationType = "Entity Updated"; - } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) { - relationType = "Entity Deleted"; - } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED)) { - relationType = "Entity Assigned"; - } else if (msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) { - relationType = "Entity Unassigned"; - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { - relationType = "Attributes Updated"; - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { - relationType = "Attributes Deleted"; - } else if (msg.getType().equals(DataConstants.ALARM_ACK)) { - relationType = "Alarm Acknowledged"; - } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { - relationType = "Alarm Cleared"; - } else if (msg.getType().equals(DataConstants.ALARM_ASSIGNED)) { - relationType = "Alarm Assigned"; - } else if (msg.getType().equals(DataConstants.ALARM_UNASSIGNED)) { - relationType = "Alarm Unassigned"; - } else if (msg.getType().equals(DataConstants.COMMENT_CREATED)) { - relationType = "Comment Created"; - } else if (msg.getType().equals(DataConstants.COMMENT_UPDATED)) { - relationType = "Comment Updated"; - } else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) { - relationType = "RPC Request to Device"; - } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED_FROM_TENANT)) { - relationType = "Entity Assigned From Tenant"; - } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED_TO_TENANT)) { - relationType = "Entity Assigned To Tenant"; - } else if (msg.getType().equals(DataConstants.TIMESERIES_UPDATED)) { - relationType = "Timeseries Updated"; - } else if (msg.getType().equals(DataConstants.TIMESERIES_DELETED)) { - relationType = "Timeseries Deleted"; - } else if (msg.getType().equals(DataConstants.RPC_QUEUED)) { - relationType = "RPC Queued"; - } else if (msg.getType().equals(DataConstants.RPC_SENT)) { - relationType = "RPC Sent"; - } else if (msg.getType().equals(DataConstants.RPC_DELIVERED)) { - relationType = "RPC Delivered"; - } else if (msg.getType().equals(DataConstants.RPC_SUCCESSFUL)) { - relationType = "RPC Successful"; - } else if (msg.getType().equals(DataConstants.RPC_TIMEOUT)) { - relationType = "RPC Timeout"; - } else if (msg.getType().equals(DataConstants.RPC_EXPIRED)) { - relationType = "RPC Expired"; - } else if (msg.getType().equals(DataConstants.RPC_FAILED)) { - relationType = "RPC Failed"; - } else if (msg.getType().equals(DataConstants.RPC_DELETED)) { - relationType = "RPC Deleted"; - } else if (msg.getType().equals(DataConstants.RELATION_ADD_OR_UPDATE)) { - relationType = "Relation Added or Updated"; - } else if (msg.getType().equals(DataConstants.RELATION_DELETED)) { - relationType = "Relation Deleted"; - } else if (msg.getType().equals(DataConstants.RELATIONS_DELETED)) { - relationType = "All Relations Deleted"; - } else { - relationType = "Other"; - } - ctx.tellNext(msg, relationType); + ctx.tellNext(msg, TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType())); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index f4e13d761e..183721e72a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -21,6 +21,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -29,11 +30,12 @@ import org.thingsboard.server.common.msg.TbMsg; @Slf4j @RuleNode( type = ComponentType.FILTER, - name = "entity type", + name = "entity type filter", configClazz = TbOriginatorTypeFilterNodeConfiguration.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Filter incoming messages by the type of message originator entity", - nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.", + nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeOriginatorTypeConfig") public class TbOriginatorTypeFilterNode implements TbNode { @@ -48,7 +50,7 @@ public class TbOriginatorTypeFilterNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { EntityType originatorType = msg.getOriginator().getEntityType(); - ctx.tellNext(msg, config.getOriginatorTypes().contains(originatorType) ? "True" : "False"); + ctx.tellNext(msg, config.getOriginatorTypes().contains(originatorType) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index a920af5066..17b7d575f5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -19,8 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -29,55 +27,17 @@ import org.thingsboard.server.common.data.plugin.ComponentType; type = ComponentType.FILTER, name = "entity type switch", configClazz = EmptyNodeConfiguration.class, - relationTypes = {"Device", "Asset", "Alarm", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node", "Edge"}, + relationTypes = {}, // should always be empty. We add the relation types for this node in AnnotationComponentDiscoveryService. nodeDescription = "Route incoming messages by Message Originator Type", - nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).", + nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).

" + + "Output connections: Message originator type or Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbNodeEmptyConfig") public class TbOriginatorTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override - protected String getRelationType(TbContext ctx, EntityId originator) throws TbNodeException { - String relationType; - EntityType originatorType = originator.getEntityType(); - switch (originatorType) { - case TENANT: - relationType = "Tenant"; - break; - case CUSTOMER: - relationType = "Customer"; - break; - case USER: - relationType = "User"; - break; - case DASHBOARD: - relationType = "Dashboard"; - break; - case ASSET: - relationType = "Asset"; - break; - case DEVICE: - relationType = "Device"; - break; - case ENTITY_VIEW: - relationType = "Entity View"; - break; - case EDGE: - relationType = "Edge"; - break; - case RULE_CHAIN: - relationType = "Rule chain"; - break; - case RULE_NODE: - relationType = "Rule node"; - break; - case ALARM: - relationType = "Alarm"; - break; - default: - throw new TbNodeException("Unsupported originator type: " + originatorType); - } - return relationType; + protected String getRelationType(TbContext ctx, EntityId originator) { + return originator.getEntityType().getNormalName(); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index 35baaf461b..a92cafefd3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -21,7 +21,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -48,7 +48,7 @@ public class TbCheckpointNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - ctx.enqueueForTellNext(msg, queueName, TbRelationTypes.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); + ctx.enqueueForTellNext(msg, queueName, TbNodeConnectionType.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index 18d817fd94..c55113783c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -29,7 +29,6 @@ import com.google.pubsub.v1.PubsubMessage; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -105,28 +104,28 @@ public class TbPubSubNode extends TbAbstractExternalNode { ApiFuture messageIdFuture = this.pubSubClient.publish(pubsubMessageBuilder.build()); ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback() { public void onSuccess(String messageId) { - TbMsg next = processPublishResult(ctx, msg, messageId); + TbMsg next = processPublishResult(msg, messageId); tellSuccess(ctx, next); } public void onFailure(Throwable t) { - TbMsg next = processException(ctx, msg, t); + TbMsg next = processException(msg, t); tellFailure(ctx, next, t); } }, ctx.getExternalCallExecutor()); } - private TbMsg processPublishResult(TbContext ctx, TbMsg origMsg, String messageId) { + private TbMsg processPublishResult(TbMsg origMsg, String messageId) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(MESSAGE_ID, messageId); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } private Publisher initPubSubClient() throws IOException { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java index 1f1f1fd132..b263518c41 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java @@ -48,14 +48,14 @@ public abstract class AbstractGeofencingNode getConfigClazz(); protected boolean checkMatches(TbMsg msg) throws TbNodeException { - JsonElement msgDataElement = new JsonParser().parse(msg.getData()); + JsonElement msgDataElement = JsonParser.parseString(msg.getData()); if (!msgDataElement.isJsonObject()) { - throw new TbNodeException("Incoming Message is not a valid JSON object"); + throw new TbNodeException("Incoming Message is not a valid JSON object!"); } JsonObject msgDataObj = msgDataElement.getAsJsonObject(); double latitude = getValueFromMessageByName(msg, msgDataObj, config.getLatitudeKeyName()); double longitude = getValueFromMessageByName(msg, msgDataObj, config.getLongitudeKeyName()); - List perimeters = getPerimeters(msg, msgDataObj); + List perimeters = getPerimeters(msg); boolean matches = false; for (Perimeter perimeter : perimeters) { if (checkMatches(perimeter, latitude, longitude)) { @@ -74,11 +74,11 @@ public abstract class AbstractGeofencingNode getPerimeters(TbMsg msg, JsonObject msgDataObj) throws TbNodeException { + protected List getPerimeters(TbMsg msg) throws TbNodeException { if (config.isFetchPerimeterInfoFromMessageMetadata()) { if (StringUtils.isEmpty(config.getPerimeterKeyName())) { // Old configuration before "perimeterKeyName" was introduced diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java index 519a4274c1..4467dfe30b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java @@ -45,8 +45,6 @@ public class GeoUtil { private static final SpatialContext distCtx = SpatialContext.GEO; private static final JtsSpatialContext jtsCtx; - private static final JsonParser JSON_PARSER = new JsonParser(); - static { JtsSpatialContextFactory factory = new JtsSpatialContextFactory(); factory.normWrapLongitude = true; @@ -64,7 +62,7 @@ public class GeoUtil { throw new RuntimeException("Polygon string can't be empty or null!"); } - JsonArray polygonsJson = normalizePolygonsJson(JSON_PARSER.parse(polygonInString).getAsJsonArray()); + JsonArray polygonsJson = normalizePolygonsJson(JsonParser.parseString(polygonInString).getAsJsonArray()); List polygons = buildPolygonsFromJson(polygonsJson); Set holes = extractHolesFrom(polygons); polygons.removeIf(holes::contains); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java index 373118ebdd..20e7039be4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -30,7 +31,7 @@ import org.thingsboard.server.common.msg.TbMsg; type = ComponentType.FILTER, name = "gps geofencing filter", configClazz = TbGpsGeofencingFilterNodeConfiguration.class, - relationTypes = {"True", "False"}, + relationTypes = {TbNodeConnectionType.TRUE, TbNodeConnectionType.FALSE}, nodeDescription = "Filter incoming messages by GPS based geofencing", nodeDetails = "Extracts latitude and longitude parameters from the incoming message and checks them according to configured perimeter.
" + "Configuration:

" + @@ -57,14 +58,15 @@ import org.thingsboard.server.common.msg.TbMsg; "

" + "{\"latitude\": 48.198618758582384, \"longitude\": 24.65322245153503, \"radius\": 100.0, \"radiusUnit\": \"METER\" }" + "

" + - "Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;", + "Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;

" + + "Output connections: True, False, Failure", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbFilterNodeGpsGeofencingConfig") public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { - ctx.tellNext(msg, checkMatches(msg) ? "True" : "False"); + ctx.tellNext(msg, checkMatches(msg) ? TbNodeConnectionType.TRUE : TbNodeConnectionType.FALSE); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index 760eec2421..68094196d0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -28,10 +28,8 @@ import org.apache.kafka.common.header.internals.RecordHeaders; import org.springframework.util.ReflectionUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; import org.thingsboard.server.common.data.exception.ThingsboardKafkaClientError; @@ -167,24 +165,24 @@ public class TbKafkaNode extends TbAbstractExternalNode { private void processRecord(TbContext ctx, TbMsg msg, RecordMetadata metadata, Exception e) { if (e == null) { - tellSuccess(ctx, processResponse(ctx, msg, metadata)); + tellSuccess(ctx, processResponse(msg, metadata)); } else { - tellFailure(ctx, processException(ctx, msg, e), e); + tellFailure(ctx, processException(msg, e), e); } } - private TbMsg processResponse(TbContext ctx, TbMsg origMsg, RecordMetadata recordMetadata) { + private TbMsg processResponse(TbMsg origMsg, RecordMetadata recordMetadata) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(OFFSET, String.valueOf(recordMetadata.offset())); metaData.putValue(PARTITION, String.valueOf(recordMetadata.partition())); metaData.putValue(TOPIC, recordMetadata.topic()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Exception e) { + private TbMsg processException(TbMsg origMsg, Exception e) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index 2af4978766..a52641bedc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbEmail; @@ -28,6 +27,8 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -35,9 +36,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; -import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE; - @Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, @@ -69,7 +67,7 @@ public class TbMsgToEmailNode implements TbNode { try { TbEmail email = convert(msg); TbMsg emailMsg = buildEmailMsg(ctx, msg, email); - ctx.tellNext(emailMsg, SUCCESS); + ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS); } catch (Exception ex) { log.warn("Can not convert message to email " + ex.getMessage()); ctx.tellFailure(msg, ex); @@ -78,7 +76,7 @@ public class TbMsgToEmailNode implements TbNode { private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) throws JsonProcessingException { String emailJson = JacksonUtil.toString(email); - return ctx.transformMsg(msg, SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson); + return ctx.transformMsg(msg, TbMsgType.SEND_EMAIL, msg.getOriginator(), msg.getMetaData().copy(), emailJson); } private TbEmail convert(TbMsg msg) throws IOException { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java index 3e2d96b8f4..2f7aaf257d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java @@ -16,9 +16,8 @@ package org.thingsboard.rule.engine.mail; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.StringUtils; import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbEmail; @@ -26,6 +25,8 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -50,7 +51,6 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; public class TbSendEmailNode extends TbAbstractExternalNode { private static final String MAIL_PROP = "mail."; - static final String SEND_EMAIL_TYPE = "SEND_EMAIL"; private TbSendEmailNodeConfiguration config; private JavaMailSenderImpl mailSender; @@ -70,7 +70,7 @@ public class TbSendEmailNode extends TbAbstractExternalNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { try { - validateType(msg.getType()); + validateType(msg); TbEmail email = getEmail(msg); var tbMsg = ackIfNeeded(ctx, msg); withCallback(ctx.getMailExecutor().executeAsync(() -> { @@ -100,8 +100,9 @@ public class TbSendEmailNode extends TbAbstractExternalNode { return email; } - private void validateType(String type) { - if (!SEND_EMAIL_TYPE.equals(type)) { + private void validateType(TbMsg msg) { + if (!msg.isTypeOf(TbMsgType.SEND_EMAIL)) { + String type = msg.getType(); log.warn("Not expected msg type [{}] for SendEmail Node", type); throw new IllegalStateException("Not expected msg type " + type + " for SendEmail Node"); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 33692decc2..fe6deebbcf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -248,7 +248,7 @@ public class TbMathNode implements TbNode { } else { md.putValue(mathResultKey, Double.toString(toDoubleValue(mathResultDef, result))); } - return TbMsg.transformMsg(msg, md); + return TbMsg.transformMsgMetadata(msg, md); } private double calculateResult(List args) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 1a2ba6d646..4c07187059 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -29,9 +29,10 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.math.BigDecimal; @@ -45,11 +46,12 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode(type = ComponentType.ENRICHMENT, - name = "calculate delta", relationTypes = {"Success", "Failure", "Other"}, + name = "calculate delta", relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, configClazz = CalculateDeltaNodeConfiguration.class, nodeDescription = "Calculates delta and amount of time passed between previous timeseries key reading " + "and current value for this key from the incoming message", - nodeDetails = "Useful for metering use cases, when you need to calculate consumption based on pulse counter reading.", + nodeDetails = "Useful for metering use cases, when you need to calculate consumption based on pulse counter reading.

" + + "Output connections: Success, Other or Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeCalculateDeltaConfig") public class CalculateDeltaNode implements TbNode { @@ -73,14 +75,14 @@ public class CalculateDeltaNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (!msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { - ctx.tellNext(msg, "Other"); + if (!msg.isTypeOf(TbMsgType.POST_TELEMETRY_REQUEST)) { + ctx.tellNext(msg, TbNodeConnectionType.OTHER); return; } JsonNode json = JacksonUtil.toJsonNode(msg.getData()); String inputKey = config.getInputValueKey(); if (!json.has(inputKey)) { - ctx.tellNext(msg, "Other"); + ctx.tellNext(msg, TbNodeConnectionType.OTHER); return; } withCallback(getLastValue(msg.getOriginator()), @@ -114,7 +116,7 @@ public class CalculateDeltaNode implements TbNode { long period = previousData != null ? currentTs - previousData.ts : 0; result.put(config.getPeriodValueKey(), period); } - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(result))); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(result))); }, t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java index 14c27a7ac7..2c14d1bea9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java @@ -16,10 +16,11 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; +import org.thingsboard.rule.engine.util.TbMsgSource; @Data public abstract class TbAbstractFetchToNodeConfiguration { - private FetchTo fetchTo; + private TbMsgSource fetchTo; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index ffa735fd4f..3ec1fd4921 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -15,6 +15,7 @@ */ package org.thingsboard.rule.engine.metadata; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -27,6 +28,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -66,7 +68,7 @@ public abstract class TbAbstractGetAttributesNode safePutAttributes(ctx, msg, msgDataAsObjectNode, entityId), @@ -75,6 +77,20 @@ public abstract class TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg); + protected TbPair upgradeRuleNodesWithOldPropertyToUseFetchTo( + JsonNode oldConfiguration, + String oldProperty, + String ifTrue, + String ifFalse + ) throws TbNodeException { + var newConfigObjectNode = (ObjectNode) oldConfiguration; + if (!newConfigObjectNode.has(oldProperty)) { + newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, TbMsgSource.METADATA.name()); + return new TbPair<>(true, newConfigObjectNode); + } + return upgradeConfigurationToUseFetchTo(oldProperty, ifTrue, ifFalse, newConfigObjectNode); + } + private void safePutAttributes(TbContext ctx, TbMsg msg, ObjectNode msgDataNode, T entityId) { Set>> failuresPairSet = ConcurrentHashMap.newKeySet(); var getKvEntryPairFutures = Futures.allAsList( @@ -150,7 +166,7 @@ public abstract class TbAbstractGetAttributesNode extends Tb @Override public void onMsg(TbContext ctx, TbMsg msg) { - var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null; + var msgDataAsObjectNode = TbMsgSource.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null; withCallback(findEntityAsync(ctx, msg.getOriginator()), entityId -> processDataAndTell(ctx, msg, entityId, msgDataAsObjectNode), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); @@ -89,7 +90,7 @@ public abstract class TbAbstractGetEntityDataNode extends Tb } else { throw new TbNodeException("property to update: '" + OLD_DATA_TO_FETCH_PROPERTY_NAME + "' has unexpected value: " + value + ". Allowed values: true or false!"); } - newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, FetchTo.METADATA.name()); + newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, TbMsgSource.METADATA.name()); return new TbPair<>(true, newConfigObjectNode); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java index 14227cadc4..e9b0cecd1a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.ContactBased; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.msg.TbMsg; @@ -37,7 +38,7 @@ public abstract class TbAbstractGetEntityDetailsNode ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); @@ -49,7 +50,7 @@ public abstract class TbAbstractGetEntityDetailsNode detailsList) throws TbNodeException { if (detailsList == null || detailsList.isEmpty()) { - throw new TbNodeException("No entity details selected!"); + throw new TbNodeException("At least one entity detail should be selected!"); } } @@ -114,9 +115,9 @@ public abstract class TbAbstractGetEntityDetailsNode upgradeConfigurationToUseFetchTo( + String oldProperty, String ifTrue, + String ifFalse, ObjectNode newConfig + ) throws TbNodeException { + var value = newConfig.get(oldProperty).asText(); if ("true".equals(value)) { - newConfigObjectNode.remove(oldProperty); - newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifTrue); - return new TbPair<>(true, newConfigObjectNode); + newConfig.remove(oldProperty); + newConfig.put(FETCH_TO_PROPERTY_NAME, ifTrue); + return new TbPair<>(true, newConfig); } else if ("false".equals(value)) { - newConfigObjectNode.remove(oldProperty); - newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifFalse); - return new TbPair<>(true, newConfigObjectNode); + newConfig.remove(oldProperty); + newConfig.put(FETCH_TO_PROPERTY_NAME, ifFalse); + return new TbPair<>(true, newConfig); } else { - throw new TbNodeException("property to update: '" + oldProperty + "' has unexpected value: " + value + ". Allowed values: true or false!"); + throw new TbNodeException("property to update: '" + oldProperty + "' has unexpected value: " + + value + ". Allowed values: true or false!"); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java index e23da1e364..c4770d625f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -41,7 +42,9 @@ import java.util.concurrent.ExecutionException; nodeDescription = "Adds device credentials to the message or message metadata", nodeDetails = "if message originator type is Device and device credentials was successfully fetched, " + "rule node enriches message or message metadata with credentialsType and credentials properties. " + - "Useful when you need to fetch device credentials and use them for further message processing. For example, use device credentials to interact with external systems.", + "Useful when you need to fetch device credentials and use them for further message processing. " + + "For example, use device credentials to interact with external systems.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig") public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo { @@ -57,7 +60,7 @@ public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java index 038e6c1c63..70712b4370 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; @Data @EqualsAndHashCode(callSuper = true) @@ -28,7 +29,7 @@ public class TbFetchDeviceCredentialsNodeConfiguration extends TbAbstractFetchTo @Override public TbFetchDeviceCredentialsNodeConfiguration defaultConfiguration() { var configuration = new TbFetchDeviceCredentialsNodeConfiguration(); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index 975da84510..9fbe5eb666 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; @@ -40,7 +41,8 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Adds attributes and/or latest timeseries data for the message originator to the message or message metadata", nodeDetails = "Useful when you need to retrieve some attributes or the latest telemetry readings from the message originator " + "that are not included in the incoming message to use them for further message processing. " + - "For example to filter messages based on the threshold value stored in the attributes.", + "For example to filter messages based on the threshold value stored in the attributes.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") public class TbGetAttributesNode extends TbAbstractGetAttributesNode { @@ -61,8 +63,8 @@ public class TbGetAttributesNode extends TbAbstractGetAttributesNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java index 2450796b59..fe15ff817f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.Collections; import java.util.List; @@ -47,7 +48,7 @@ public class TbGetAttributesNodeConfiguration extends TbAbstractFetchToNodeConfi configuration.setLatestTsKeyNames(Collections.emptyList()); configuration.setTellFailureIfAbsent(true); configuration.setGetLatestValueWithTs(false); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index 3cf69b5e26..088aa596fc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -39,7 +39,8 @@ import org.thingsboard.server.common.data.util.TbPair; nodeDescription = "Adds message originator customer attributes or latest telemetry into message or message metadata", nodeDetails = "Useful in multi-customer solutions where each customer has a different configuration or threshold set " + "that is stored as customer attributes or telemetry data and used for dynamic message filtering, transformation, " + - "or actions such as alarm creation if the threshold is exceeded.", + "or actions such as alarm creation if the threshold is exceeded.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeCustomerAttributesConfig") public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode { @@ -64,9 +65,7 @@ public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { - return fromVersion == 0 ? - upgradeToUseFetchToAndDataToFetch(oldConfiguration) : - new TbPair<>(false, oldConfiguration); + return fromVersion == 0 ? upgradeToUseFetchToAndDataToFetch(oldConfiguration) : new TbPair<>(false, oldConfiguration); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index 3be5ac15c3..f0b391ca54 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; @@ -47,7 +48,8 @@ import java.util.NoSuchElementException; version = 1, nodeDescription = "Adds message originator customer details into message or message metadata", nodeDetails = "Useful in multi-customer solutions where we need dynamically use customer contact information " + - "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.", + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeEntityDetailsConfig") public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { @@ -111,8 +113,8 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java index c49a1d3c30..370e7cd5a3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.Collections; @@ -29,7 +30,7 @@ public class TbGetCustomerDetailsNodeConfiguration extends TbAbstractGetEntityDe public TbGetCustomerDetailsNodeConfiguration defaultConfiguration() { var configuration = new TbGetCustomerDetailsNodeConfiguration(); configuration.setDetailsList(Collections.emptyList()); - configuration.setFetchTo(FetchTo.DATA); + configuration.setFetchTo(TbMsgSource.DATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index 67e544fcfa..ac88005f51 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -25,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; @@ -38,7 +39,9 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Add originators related device attributes and/or latest telemetry values into message or message metadata", nodeDetails = "Related device lookup based on the configured relation query. " + "If multiple related devices are found, only first device is used for message enrichment, other entities are discarded. " + - "Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation to the message originator and use them for further message processing.", + "Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation " + + "to the message originator and use them for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeDeviceAttributesConfig") public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode { @@ -64,8 +67,8 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java index 60a2c9cdf7..e6be83eb0c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.data.DeviceRelationsQuery; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -38,7 +39,7 @@ public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfigu configuration.setLatestTsKeyNames(Collections.emptyList()); configuration.setTellFailureIfAbsent(true); configuration.setGetLatestValueWithTs(false); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); var deviceRelationsQuery = new DeviceRelationsQuery(); deviceRelationsQuery.setDirection(EntitySearchDirection.FROM); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java index eb3f3b805c..ec57cba3d2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.HashMap; @@ -34,7 +35,7 @@ public class TbGetEntityDataNodeConfiguration extends TbGetMappedDataNodeConfigu dataMapping.putIfAbsent("alarmThreshold", "threshold"); configuration.setDataMapping(dataMapping); configuration.setDataToFetch(DataToFetch.ATTRIBUTES); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java index 45ca8e19c0..6c9f78e9b4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.HashMap; import java.util.Map; @@ -37,7 +38,7 @@ public class TbGetOriginatorFieldsConfiguration extends TbGetMappedDataNodeConfi dataMapping.put("type", "originatorType"); configuration.setDataMapping(dataMapping); configuration.setIgnoreNullStrings(false); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java index 34153360a3..cb7877d99a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; @@ -40,7 +41,8 @@ import java.util.concurrent.ExecutionException; version = 1, nodeDescription = "Adds message originator fields values into message or message metadata", nodeDetails = "Fetches fields values specified in the mapping. If specified field is not part of originator fields it will be ignored. " + - "Useful when you need to retrieve originator fields and use them for further message processing.", + "Useful when you need to retrieve originator fields and use them for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeOriginatorFieldsConfig") public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode { @@ -57,7 +59,7 @@ public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode(true, newConfigObjectNode); } return new TbPair<>(false, oldConfiguration); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index 249caa87ac..50e75c8cc0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -40,7 +40,8 @@ import java.util.Arrays; nodeDescription = "Adds originators related entity attributes or latest telemetry or fields into message or message metadata", nodeDetails = "Related entity lookup based on the configured relation query. " + "If multiple related entities are found, only first entity is used for message enrichment, other entities are discarded. " + - "Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.", + "Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeRelatedAttributesConfig") public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java index 4c78915ae4..4cb0723365 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.data.RelationsQuery; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; @@ -38,7 +39,7 @@ public class TbGetRelatedDataNodeConfiguration extends TbGetEntityDataNodeConfig dataMapping.putIfAbsent("serialNumber", "sn"); configuration.setDataMapping(dataMapping); configuration.setDataToFetch(DataToFetch.ATTRIBUTES); - configuration.setFetchTo(FetchTo.METADATA); + configuration.setFetchTo(TbMsgSource.METADATA); var relationsQuery = new RelationsQuery(); var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index 569c76c2a2..1bf1b07530 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -37,16 +37,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST; -import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; - /** * Created by mshvayka on 04.09.18. */ @@ -57,7 +54,8 @@ import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfigurati nodeDescription = "Adds message originator telemetry for selected time range into message metadata", nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " + "instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " + - "Also, this node can be used for telemetry aggregation within configured fetch interval.", + "Also, this node can be used for telemetry aggregation within configured fetch interval.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase") public class TbGetTelemetryNode implements TbNode { @@ -76,7 +74,7 @@ public class TbGetTelemetryNode implements TbNode { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class); tsKeyNames = config.getLatestTsKeyNames(); - limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? validateLimit(config.getLimit()) : 1; + limit = config.getFetchMode().equals(TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL) ? validateLimit(config.getLimit()) : 1; fetchMode = config.getFetchMode(); orderByFetchAll = config.getOrderBy(); if (StringUtils.isEmpty(orderByFetchAll)) { @@ -86,7 +84,7 @@ public class TbGetTelemetryNode implements TbNode { } Aggregation parseAggregationConfig(String aggName) { - if (StringUtils.isEmpty(aggName) || !fetchMode.equals(FETCH_MODE_ALL)) { + if (StringUtils.isEmpty(aggName) || !fetchMode.equals(TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL)) { return Aggregation.NONE; } return Aggregation.valueOf(aggName); @@ -102,8 +100,8 @@ public class TbGetTelemetryNode implements TbNode { List keys = TbNodeUtils.processPatterns(tsKeyNames, msg); ListenableFuture> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(interval, keys)); DonAsynchron.withCallback(list, data -> { - process(data, msg, keys); - ctx.tellSuccess(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData())); + var metaData = updateMetadata(data, msg, keys); + ctx.tellSuccess(TbMsg.transformMsgMetadata(msg, metaData)); }, error -> ctx.tellFailure(msg, error), ctx.getDbCallbackExecutor()); } catch (Exception e) { ctx.tellFailure(msg, e); @@ -124,28 +122,29 @@ public class TbGetTelemetryNode implements TbNode { private String getOrderBy() { switch (fetchMode) { - case FETCH_MODE_ALL: + case TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL: return orderByFetchAll; - case FETCH_MODE_FIRST: + case TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST: return ASC_ORDER; default: return DESC_ORDER; } } - private void process(List entries, TbMsg msg, List keys) { + private TbMsgMetaData updateMetadata(List entries, TbMsg msg, List keys) { ObjectNode resultNode = JacksonUtil.newObjectNode(JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER); - if (FETCH_MODE_ALL.equals(fetchMode)) { + if (TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL.equals(fetchMode)) { entries.forEach(entry -> processArray(resultNode, entry)); } else { entries.forEach(entry -> processSingle(resultNode, entry)); } - + var copy = msg.getMetaData().copy(); for (String key : keys) { if (resultNode.has(key)) { - msg.getMetaData().putValue(key, resultNode.get(key).toString()); + copy.putValue(key, resultNode.get(key).toString()); } } + return copy; } private void processSingle(ObjectNode node, TsKvEntry entry) { @@ -216,7 +215,7 @@ public class TbGetTelemetryNode implements TbNode { if (limit != 0) { return limit; } else { - return MAX_FETCH_SIZE; + return TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java index cbbf00ce92..bcd54cd829 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java @@ -37,7 +37,8 @@ import org.thingsboard.server.common.data.util.TbPair; version = 1, nodeDescription = "Adds message originator tenant attributes or latest telemetry into message or message metadata", nodeDetails = "Useful when you need to retrieve some common configuration or threshold set " + - "that is stored as tenant attributes or telemetry data and use it for further message processing.", + "that is stored as tenant attributes or telemetry data and use it for further message processing.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeTenantAttributesConfig") public class TbGetTenantAttributeNode extends TbAbstractGetEntityDataNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java index 8e293aea37..54d95645e1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -36,7 +37,8 @@ import org.thingsboard.server.common.msg.TbMsg; version = 1, nodeDescription = "Adds message originator tenant details into message or message metadata", nodeDetails = "Useful when we need to retrieve contact information from your tenant " + - "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.", + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

" + + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeEntityDetailsConfig") public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode { @@ -66,8 +68,8 @@ public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode(false, oldConfiguration); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java index 35cdc42a54..f50f44ec23 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.util.TbMsgSource; import java.util.Collections; @@ -29,7 +30,7 @@ public class TbGetTenantDetailsNodeConfiguration extends TbAbstractGetEntityDeta public TbGetTenantDetailsNodeConfiguration defaultConfiguration() { var configuration = new TbGetTenantDetailsNodeConfiguration(); configuration.setDetailsList(Collections.emptyList()); - configuration.setFetchTo(FetchTo.DATA); + configuration.setFetchTo(TbMsgSource.DATA); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 121b9fb756..23d2132a1b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -25,7 +25,6 @@ import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -41,6 +40,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.net.ssl.SSLException; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -58,7 +58,7 @@ import java.util.concurrent.TimeoutException; ) public class TbMqttNode extends TbAbstractExternalNode { - private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset UTF8 = StandardCharsets.UTF_8; private static final String ERROR = "error"; @@ -86,16 +86,16 @@ public class TbMqttNode extends TbAbstractExternalNode { if (future.isSuccess()) { tellSuccess(ctx, tbMsg); } else { - tellFailure(ctx, processException(ctx, tbMsg, future.cause()), future.cause()); + tellFailure(ctx, processException(tbMsg, future.cause()), future.cause()); } } ); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { + private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java index f1fd9727fa..5f1bea2bdb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java @@ -19,7 +19,6 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -77,7 +76,7 @@ public class TbNotificationNode extends TbAbstractExternalNode { ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { TbMsgMetaData metaData = tbMsg.getMetaData().copy(); metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); - tellSuccess(ctx, TbMsg.transformMsg(tbMsg, metaData)); + tellSuccess(ctx, TbMsg.transformMsgMetadata(tbMsg, metaData)); })), r -> { }, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 70f15935fc..9e2719bf53 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -189,7 +190,7 @@ class AlarmState { metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); } setAlarmConditionMetadata(ruleState, metaData); - TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : null, "ALARM", + TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : null, TbMsgType.ALARM, originator, msg != null ? msg.getCustomerId() : null, metaData, data); ctx.enqueueForTellNext(newMsg, relationType); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index 31f661417b..2aeb4eedf5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.dao.sql.query.EntityKeyMapping; @@ -55,6 +54,18 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_DELETE; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_ASSIGNED; +import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_UNASSIGNED; +import static org.thingsboard.server.common.data.msg.TbMsgType.INACTIVITY_EVENT; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; + @Slf4j class DeviceState { @@ -136,24 +147,24 @@ class DeviceState { latestValues = fetchLatestValues(ctx, deviceId); } boolean stateChanged = false; - if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { + if (msg.isTypeOf(POST_TELEMETRY_REQUEST)) { stateChanged = processTelemetry(ctx, msg); - } else if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { + } else if (msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { stateChanged = processAttributesUpdateRequest(ctx, msg); - } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT) || msg.getType().equals(DataConstants.INACTIVITY_EVENT)) { + } else if (msg.isTypeOneOf(ACTIVITY_EVENT, INACTIVITY_EVENT)) { stateChanged = processDeviceActivityEvent(ctx, msg); - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { + } else if (msg.isTypeOf(ATTRIBUTES_UPDATED)) { stateChanged = processAttributesUpdateNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { + } else if (msg.isTypeOf(ATTRIBUTES_DELETED)) { stateChanged = processAttributesDeleteNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { + } else if (msg.isTypeOf(ALARM_CLEAR)) { stateChanged = processAlarmClearNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ALARM_ACK)) { + } else if (msg.isTypeOf(ALARM_ACK)) { processAlarmAckNotification(ctx, msg); - } else if (msg.getType().equals(DataConstants.ALARM_DELETE)) { + } else if (msg.isTypeOf(ALARM_DELETE)) { processAlarmDeleteNotification(ctx, msg); } else { - if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED) || msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) { + if (msg.isTypeOneOf(ENTITY_ASSIGNED, ENTITY_UNASSIGNED)) { dynamicPredicateValueCtx.resetCustomer(); } ctx.tellSuccess(msg); @@ -213,7 +224,7 @@ class DeviceState { private boolean processAttributesDeleteNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { boolean stateChanged = false; List keys = new ArrayList<>(); - new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); + JsonParser.parseString(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); String scope = msg.getMetaData().getValue(DataConstants.SCOPE); if (StringUtils.isEmpty(scope)) { scope = DataConstants.CLIENT_SCOPE; @@ -241,7 +252,7 @@ class DeviceState { private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); + Set attributes = JsonConverter.convertToAttributes(JsonParser.parseString(msg.getData())); if (!attributes.isEmpty()) { SnapshotUpdate update = merge(latestValues, attributes, scope); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { @@ -256,7 +267,7 @@ class DeviceState { protected boolean processTelemetry(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { boolean stateChanged = false; - Map> tsKvMap = JsonConverter.convertToSortedTelemetry(new JsonParser().parse(msg.getData()), msg.getMetaDataTs()); + Map> tsKvMap = JsonConverter.convertToSortedTelemetry(JsonParser.parseString(msg.getData()), msg.getMetaDataTs()); // iterate over data by ts (ASC order). for (Map.Entry> entry : tsKvMap.entrySet()) { Long ts = entry.getKey(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index e2986677b5..5404b2ea0e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -26,12 +26,12 @@ import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -60,9 +60,6 @@ import java.util.concurrent.TimeUnit; configDirective = "tbDeviceProfileConfig" ) public class TbDeviceProfileNode implements TbNode { - private static final String PERIODIC_MSG_TYPE = "TbDeviceProfilePeriodicMsg"; - private static final String PROFILE_UPDATE_MSG_TYPE = "TbDeviceProfileUpdateMsg"; - private static final String DEVICE_UPDATE_MSG_TYPE = "TbDeviceUpdateMsg"; private TbDeviceProfileNodeConfiguration config; private RuleEngineDeviceProfileCache cache; @@ -111,12 +108,12 @@ public class TbDeviceProfileNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { EntityType originatorType = msg.getOriginator().getEntityType(); - if (msg.getType().equals(PERIODIC_MSG_TYPE)) { + if (msg.isTypeOf(TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG)) { scheduleAlarmHarvesting(ctx, msg); harvestAlarms(ctx, System.currentTimeMillis()); - } else if (msg.getType().equals(PROFILE_UPDATE_MSG_TYPE)) { + } else if (msg.isTypeOf(TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG)) { updateProfile(ctx, new DeviceProfileId(UUID.fromString(msg.getData()))); - } else if (msg.getType().equals(DEVICE_UPDATE_MSG_TYPE)) { + } else if (msg.isTypeOf(TbMsgType.DEVICE_UPDATE_SELF_MSG)) { JsonNode data = JacksonUtil.toJsonNode(msg.getData()); DeviceId deviceId = new DeviceId(UUID.fromString(data.get("deviceId").asText())); if (data.has("profileId")) { @@ -127,10 +124,10 @@ public class TbDeviceProfileNode implements TbNode { } else { if (EntityType.DEVICE.equals(originatorType)) { DeviceId deviceId = new DeviceId(msg.getOriginator().getId()); - if (msg.getType().equals(DataConstants.ENTITY_UPDATED)) { + if (msg.isTypeOf(TbMsgType.ENTITY_UPDATED)) { invalidateDeviceProfileCache(deviceId, msg.getData()); ctx.tellSuccess(msg); - } else if (msg.getType().equals(DataConstants.ENTITY_DELETED)) { + } else if (msg.isTypeOf(TbMsgType.ENTITY_DELETED)) { removeDeviceState(deviceId); ctx.tellSuccess(msg); } else { @@ -177,7 +174,7 @@ public class TbDeviceProfileNode implements TbNode { } protected void scheduleAlarmHarvesting(TbContext ctx, TbMsg msg) { - TbMsg periodicCheck = TbMsg.newMsg(PERIODIC_MSG_TYPE, ctx.getTenantId(), msg != null ? msg.getCustomerId() : null, TbMsgMetaData.EMPTY, "{}"); + TbMsg periodicCheck = TbMsg.newMsg(TbMsgType.DEVICE_PROFILE_PERIODIC_SELF_MSG, ctx.getTenantId(), msg != null ? msg.getCustomerId() : null, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); ctx.tellSelf(periodicCheck, TimeUnit.MINUTES.toMillis(1)); } @@ -202,7 +199,7 @@ public class TbDeviceProfileNode implements TbNode { } protected void onProfileUpdate(DeviceProfile profile) { - ctx.tellSelf(TbMsg.newMsg(PROFILE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L); + ctx.tellSelf(TbMsg.newMsg(TbMsgType.DEVICE_PROFILE_UPDATE_SELF_MSG, ctx.getTenantId(), TbMsgMetaData.EMPTY, profile.getId().getId().toString()), 0L); } private void onDeviceUpdate(DeviceId deviceId, DeviceProfile deviceProfile) { @@ -211,7 +208,7 @@ public class TbDeviceProfileNode implements TbNode { if (deviceProfile != null) { msgData.put("deviceProfileId", deviceProfile.getId().getId().toString()); } - ctx.tellSelf(TbMsg.newMsg(DEVICE_UPDATE_MSG_TYPE, ctx.getTenantId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(msgData)), 0L); + ctx.tellSelf(TbMsg.newMsg(TbMsgType.DEVICE_UPDATE_SELF_MSG, ctx.getTenantId(), TbMsgMetaData.EMPTY, JacksonUtil.toString(msgData)), 0L); } protected void invalidateDeviceProfileCache(DeviceId deviceId, String deviceJson) { @@ -224,7 +221,7 @@ public class TbDeviceProfileNode implements TbNode { removeDeviceState(deviceId); } } catch (IllegalArgumentException e) { - log.debug("[{}] Received device update notification with non-device msg body: [{}][{}]", ctx.getSelfId(), deviceId, e); + log.debug("[{}] Received device update notification with non-device msg body: [{}]", ctx.getSelfId(), deviceId, e); } } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index 4b42ee1d18..be7a22f713 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -24,7 +24,6 @@ import com.rabbitmq.client.MessageProperties; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; @@ -35,6 +34,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import static org.thingsboard.common.util.DonAsynchron.withCallback; @@ -51,7 +51,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; ) public class TbRabbitMqNode extends TbAbstractExternalNode { - private static final Charset UTF8 = Charset.forName("UTF-8"); + private static final Charset UTF8 = StandardCharsets.UTF_8; private static final String ERROR = "error"; @@ -87,7 +87,7 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { var tbMsg = ackIfNeeded(ctx, msg); withCallback(publishMessageAsync(ctx, tbMsg), m -> tellSuccess(ctx, m), - t -> tellFailure(ctx, processException(ctx, tbMsg, t), t)); + t -> tellFailure(ctx, processException(tbMsg, t), t)); } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { @@ -115,10 +115,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { return msg; } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable t) { + private TbMsg processException(TbMsg origMsg, Throwable t) { TbMsgMetaData metaData = origMsg.getMetaData().copy(); metaData.putValue(ERROR, t.getClass() + ": " + t.getMessage()); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 28e221b0b1..0c9eca5f0a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -43,7 +43,6 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials; @@ -204,7 +203,7 @@ public class TbHttpClient { future.addCallback(new ListenableFutureCallback<>() { @Override public void onFailure(Throwable throwable) { - onFailure.accept(processException(ctx, msg, throwable), throwable); + onFailure.accept(processException(msg, throwable), throwable); } @Override @@ -212,7 +211,7 @@ public class TbHttpClient { if (responseEntity.getStatusCode().is2xxSuccessful()) { onSuccess.accept(processResponse(ctx, msg, responseEntity)); } else { - onFailure.accept(processFailureResponse(ctx, msg, responseEntity), null); + onFailure.accept(processFailureResponse(msg, responseEntity), null); } } }); @@ -261,8 +260,8 @@ public class TbHttpClient { metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); headersToMetaData(response.getHeaders(), metaData::putValue); - String body = response.getBody() == null ? "{}" : response.getBody(); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, body); + String body = response.getBody() == null ? TbMsg.EMPTY_JSON_OBJECT : response.getBody(); + return ctx.transformMsg(origMsg, metaData, body); } void headersToMetaData(Map> headers, BiConsumer consumer) { @@ -280,17 +279,17 @@ public class TbHttpClient { }); } - private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) { + private TbMsg processFailureResponse(TbMsg origMsg, ResponseEntity response) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(STATUS, response.getStatusCode().name()); metaData.putValue(STATUS_CODE, response.getStatusCode().value() + ""); metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase()); metaData.putValue(ERROR_BODY, response.getBody()); headersToMetaData(response.getHeaders(), metaData::putValue); - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } - private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { + private TbMsg processException(TbMsg origMsg, Throwable e) { TbMsgMetaData metaData = origMsg.getMetaData(); metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage()); if (e instanceof RestClientResponseException) { @@ -299,7 +298,7 @@ public class TbHttpClient { metaData.putValue(STATUS_CODE, restClientResponseException.getRawStatusCode() + ""); metaData.putValue(ERROR_BODY, restClientResponseException.getResponseBodyAsString()); } - return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); + return TbMsg.transformMsgMetadata(origMsg, metaData); } private HttpHeaders prepareHeaders(TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index c6083f1098..281f65e1e4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.rest; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 79bd6f9bf9..4ae8d0e78e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -27,12 +27,13 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -56,7 +57,6 @@ public class TbSendRPCRequestNode implements TbNode { private Random random = new Random(); private Gson gson = new Gson(); - private JsonParser jsonParser = new JsonParser(); private TbSendRpcRequestNodeConfiguration config; @Override @@ -66,7 +66,7 @@ public class TbSendRPCRequestNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - JsonObject json = jsonParser.parse(msg.getData()).getAsJsonObject(); + JsonObject json = JsonParser.parseString(msg.getData()).getAsJsonObject(); String tmp; if (msg.getOriginator().getEntityType() != EntityType.DEVICE) { ctx.tellFailure(msg, new RuntimeException("Message originator is not a device entity!")); @@ -76,7 +76,7 @@ public class TbSendRPCRequestNode implements TbNode { ctx.tellFailure(msg, new RuntimeException("Params are not present in the message!")); } else { int requestId = json.has("requestId") ? json.get("requestId").getAsInt() : random.nextInt(); - boolean restApiCall = msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE); + boolean restApiCall = msg.isTypeOf(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE); tmp = msg.getMetaData().getValue("oneway"); boolean oneway = !StringUtils.isEmpty(tmp) && Boolean.parseBoolean(tmp); @@ -116,8 +116,8 @@ public class TbSendRPCRequestNode implements TbNode { ctx.getRpcService().sendRpcRequestToDevice(request, ruleEngineDeviceRpcResponse -> { if (ruleEngineDeviceRpcResponse.getError().isEmpty()) { - TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().orElse("{}")); - ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); + TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), ruleEngineDeviceRpcResponse.getResponse().orElse(TbMsg.EMPTY_JSON_OBJECT)); + ctx.enqueueForTellNext(next, TbNodeConnectionType.SUCCESS); } else { TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 9cf9bfa1bd..ef49558631 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import java.util.ArrayList; @@ -36,6 +35,7 @@ import java.util.List; import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; @Slf4j @RuleNode( @@ -65,7 +65,7 @@ public class TbMsgAttributesNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (!msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { + if (!msg.isTypeOf(POST_ATTRIBUTES_REQUEST)) { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index dcc84813cd..799089d4a8 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import java.util.ArrayList; @@ -39,6 +38,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; + @Slf4j @RuleNode( type = ComponentType.ACTION, @@ -81,13 +82,13 @@ public class TbMsgTimeseriesNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - if (!msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { + if (!msg.isTypeOf(POST_TELEMETRY_REQUEST)) { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } long ts = computeTs(msg, config.isUseServerTs()); String src = msg.getData(); - Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); + Map> tsKvMap = JsonConverter.convertToTelemetry(JsonParser.parseString(src), ts); if (tsKvMap.isEmpty()) { ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); return; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java index 7bf08e8647..72570d4e2a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java @@ -22,7 +22,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -75,7 +75,7 @@ public abstract class TbAbstractTransformNode implements TbNode { ctx.tellFailure(msg, e); } }); - msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure)); + msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, TbNodeConnectionType.SUCCESS, wrapper::onSuccess, wrapper::onFailure)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index 5cc5dfe23f..4d1a092097 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -34,7 +34,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; @@ -74,14 +73,7 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode metaDataMap.remove(key)); + keysToDelete.forEach(metaDataMap::remove); metaData = new TbMsgMetaData(metaDataMap); } else { JsonNode dataNode = JacksonUtil.toJsonNode(msgData); @@ -94,7 +94,7 @@ public class TbDeleteKeysNode implements TbNode { if (keysToDelete.isEmpty()) { ctx.tellSuccess(msg); } else { - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, msgData)); + ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, msgData)); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java index da7d765a95..0e85a50c34 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java @@ -70,7 +70,7 @@ public class TbJsonPathNode implements TbNode { if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) { try { Object jsonPathData = jsonPath.read(msg.getData(), this.configurationJsonPath); - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(jsonPathData))); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(jsonPathData))); } catch (PathNotFoundException e) { ctx.tellFailure(msg, e); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java index 3fce7494ea..88e6dc1a8b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java @@ -74,7 +74,7 @@ public class TbRenameKeysNode implements TbNode { } metaData = new TbMsgMetaData(metaDataMap); } else { - JsonNode dataNode = JacksonUtil.toJsonNode(msg.getData()); + JsonNode dataNode = JacksonUtil.toJsonNode(data); if (dataNode.isObject()) { ObjectNode msgData = (ObjectNode) dataNode; for (Map.Entry entry : renameKeysMapping.entrySet()) { @@ -89,7 +89,7 @@ public class TbRenameKeysNode implements TbNode { } } if (msgChanged) { - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), metaData, data)); + ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, data)); } else { ctx.tellSuccess(msg); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index ae7ceade97..8959d12c9f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -25,7 +25,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -64,7 +64,7 @@ public class TbSplitArrayMsgNode implements TbNode { if (data.isEmpty()) { ctx.ack(msg); } else if (data.size() == 1) { - ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(data.get(0)))); + ctx.tellSuccess(TbMsg.transformMsgData(msg, JacksonUtil.toString(data.get(0)))); } else { TbMsgCallbackWrapper wrapper = new MultipleTbMsgsCallbackWrapper(data.size(), new TbMsgCallback() { @Override @@ -78,8 +78,8 @@ public class TbSplitArrayMsgNode implements TbNode { } }); data.forEach(msgNode -> { - TbMsg outMsg = TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(msgNode)); - ctx.enqueueForTellNext(outMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure); + TbMsg outMsg = TbMsg.transformMsgData(msg, JacksonUtil.toString(msgNode)); + ctx.enqueueForTellNext(outMsg, TbNodeConnectionType.SUCCESS, wrapper::onSuccess, wrapper::onFailure); }); } } else { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TbMsgSource.java similarity index 90% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TbMsgSource.java index 811868bef0..e73b7976fa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TbMsgSource.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.metadata; +package org.thingsboard.rule.engine.util; -public enum FetchTo { +public enum TbMsgSource { DATA, METADATA diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java new file mode 100644 index 0000000000..ed9053e2e2 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/TestDbCallbackExecutor.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.common.util.ListeningExecutor; + +import java.util.concurrent.Callable; + +public class TestDbCallbackExecutor implements ListeningExecutor { + + @Override + public ListenableFuture executeAsync(Callable task) { + try { + return Futures.immediateFuture(task.call()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void execute(Runnable command) { + command.run(); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 132383c064..33c6071769 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import org.apache.commons.lang3.NotImplementedException; import org.junit.Before; import org.junit.Test; @@ -29,15 +28,18 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; @@ -45,6 +47,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -52,7 +55,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.script.ScriptException; import java.io.IOException; -import java.util.concurrent.Callable; import java.util.function.Consumer; import static org.junit.Assert.assertEquals; @@ -68,11 +70,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.IS_CLEARED_ALARM; -import static org.thingsboard.server.common.data.DataConstants.IS_EXISTING_ALARM; -import static org.thingsboard.server.common.data.DataConstants.IS_NEW_ALARM; -import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; -import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; @RunWith(MockitoJUnitRunner.class) public class TbAlarmNodeTest { @@ -105,28 +102,14 @@ public class TbAlarmNodeTest { @Before public void before() { - dbExecutor = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(Runnable command) { - command.run(); - } - }; + dbExecutor = new TestDbCallbackExecutor(); } @Test - public void newAlarmCanBeCreated() throws ScriptException, IOException { + public void newAlarmCanBeCreated() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); @@ -136,7 +119,7 @@ public class TbAlarmNodeTest { .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagate(true) .type("SomeType") .details(null) @@ -154,16 +137,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -171,10 +154,10 @@ public class TbAlarmNodeTest { } @Test - public void buildDetailsThrowsException() throws ScriptException, IOException { + public void buildDetailsThrowsException() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFailedFuture(new NotImplementedException("message"))); when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); @@ -194,10 +177,10 @@ public class TbAlarmNodeTest { } @Test - public void ifAlarmClearedCreateNew() throws ScriptException, IOException { + public void ifAlarmClearedCreateNew() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm clearedAlarm = Alarm.builder().cleared(true).acknowledged(true).build(); @@ -209,7 +192,7 @@ public class TbAlarmNodeTest { .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagate(true) .type("SomeType") .details(null) @@ -228,16 +211,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); @@ -246,13 +229,13 @@ public class TbAlarmNodeTest { } @Test - public void alarmCanBeUpdated() throws IOException { + public void alarmCanBeUpdated() { initWithCreateAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); @@ -260,7 +243,7 @@ public class TbAlarmNodeTest { Alarm expectedAlarm = Alarm.builder() .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagate(true) .type("SomeType") .details(null) @@ -279,16 +262,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Updated")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_EXISTING_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_EXISTING_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -297,19 +280,19 @@ public class TbAlarmNodeTest { } @Test - public void alarmCanBeCleared() throws ScriptException, IOException { + public void alarmCanBeCleared() { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); Alarm expectedAlarm = Alarm.builder() .tenantId(tenantId) .originator(originator) .cleared(true) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(false) .type("SomeType") .details(null) @@ -332,16 +315,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Cleared")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_CLEARED_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -352,18 +335,18 @@ public class TbAlarmNodeTest { public void alarmCanBeClearedWithAlarmOriginator() throws ScriptException, IOException { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); AlarmId id = new AlarmId(alarmOriginator.getId()); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(AlarmSeverity.WARNING).endTs(oldEndDate).build(); activeAlarm.setId(id); Alarm expectedAlarm = Alarm.builder() .tenantId(tenantId) .originator(originator) .cleared(true) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(false) .type("SomeType") .details(null) @@ -387,16 +370,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Cleared")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(alarmOriginator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_CLEARED_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_CLEARED_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -425,14 +408,14 @@ public class TbAlarmNodeTest { String rawJson = "{\"alarmSeverity\": \"WARNING\", \"passed\": 5}"; metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm expectedAlarm = Alarm.builder() .startTs(ts) .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(true) .type("SomeType") .details(null) @@ -454,16 +437,16 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -491,14 +474,14 @@ public class TbAlarmNodeTest { node.init(ctx, nodeConfiguration); metaData.putValue("alarmSeverity", "WARNING"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm expectedAlarm = Alarm.builder() .startTs(ts) .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(WARNING) + .severity(AlarmSeverity.WARNING) .propagate(true) .type("SomeType") .details(null) @@ -520,15 +503,15 @@ public class TbAlarmNodeTest { verify(ctx).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -540,7 +523,7 @@ public class TbAlarmNodeTest { for (int i = 0; i < 10; i++) { var config = new TbCreateAlarmNodeConfiguration(); config.setPropagateToTenant(true); - config.setSeverity(CRITICAL.name()); + config.setSeverity(AlarmSeverity.CRITICAL.name()); config.setAlarmType("SomeType" + i); config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); @@ -557,14 +540,14 @@ public class TbAlarmNodeTest { node.init(ctx, nodeConfiguration); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); Alarm expectedAlarm = Alarm.builder() .startTs(ts) .endTs(ts) .tenantId(tenantId) .originator(originator) - .severity(CRITICAL) + .severity(AlarmSeverity.CRITICAL) .propagateToTenant(true) .type("SomeType" + i) .details(null) @@ -585,16 +568,16 @@ public class TbAlarmNodeTest { verify(ctx, atMost(10)).tellNext(any(), eq("Created")); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx, atMost(10)).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("ALARM", typeCaptor.getValue()); + assertEquals(TbMsgType.ALARM, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("value", metadataCaptor.getValue().getValue("key")); - assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(IS_NEW_ALARM)); + assertEquals(Boolean.TRUE.toString(), metadataCaptor.getValue().getValue(DataConstants.IS_NEW_ALARM)); assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), Alarm.class); @@ -606,7 +589,7 @@ public class TbAlarmNodeTest { try { TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration(); config.setPropagate(true); - config.setSeverity(CRITICAL.name()); + config.setSeverity(AlarmSeverity.CRITICAL.name()); config.setAlarmType("SomeType"); config.setScriptLang(ScriptLanguage.JS); config.setAlarmDetailsBuildJs("DETAILS"); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java index 22df15631e..f36cc94540 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,17 +25,19 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; -import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -47,7 +48,6 @@ import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.relation.RelationService; import java.util.Collections; -import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -58,8 +58,6 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class TbCreateRelationNodeTest { - private static final String RELATION_TYPE_CONTAINS = "Contains"; - private TbCreateRelationNode node; @Mock @@ -78,21 +76,7 @@ public class TbCreateRelationNodeTest { @Before public void before() { - dbExecutor = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(Runnable command) { - command.run(); - } - }; + dbExecutor = new TestDbCallbackExecutor(); } @Test @@ -111,15 +95,15 @@ public class TbCreateRelationNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("name", "AssetName"); metaData.putValue("type", "AssetType"); - msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); - when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(false)); - when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)))) .thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); - verify(ctx).tellNext(msg, TbRelationTypes.SUCCESS); + verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS); } @Test @@ -138,19 +122,19 @@ public class TbCreateRelationNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("name", "AssetName"); metaData.putValue("type", "AssetType"); - msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); EntityRelation relation = new EntityRelation(); - when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(Collections.singletonList(relation))); when(ctx.getRelationService().deleteRelationAsync(any(), eq(relation))).thenReturn(Futures.immediateFuture(true)); - when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(false)); - when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)))) .thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); - verify(ctx).tellNext(msg, TbRelationTypes.SUCCESS); + verify(ctx).tellNext(msg, TbNodeConnectionType.SUCCESS); } @Test @@ -169,20 +153,17 @@ public class TbCreateRelationNodeTest { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("name", "AssetName"); metaData.putValue("type", "AssetType"); - msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + msg = TbMsg.newMsg(TbMsgType.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); - when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + when(ctx.getRelationService().checkRelationAsync(any(), eq(assetId), eq(deviceId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON))) .thenReturn(Futures.immediateFuture(false)); - when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON)))) .thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); assertEquals(assetId, originatorCaptor.getValue()); } @@ -201,9 +182,9 @@ public class TbCreateRelationNodeTest { private TbCreateRelationNodeConfiguration createRelationNodeConfig() { TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration(); configuration.setDirection(EntitySearchDirection.FROM.name()); - configuration.setRelationType(RELATION_TYPE_CONTAINS); + configuration.setRelationType(EntityRelation.CONTAINS_TYPE); configuration.setEntityCacheExpiration(300); - configuration.setEntityType("ASSET"); + configuration.setEntityType(EntityType.ASSET.name()); configuration.setEntityNamePattern("${name}"); configuration.setEntityTypePattern("${type}"); configuration.setCreateEntityIfNotExists(false); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java index f12288da3a..885002a694 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -49,7 +50,7 @@ public class TbLogNodeTest { TbLogNode node = new TbLogNode(); String data = "{\"key\": \"value\"}"; TbMsgMetaData metaData = new TbMsgMetaData(Map.of("mdKey1", "mdValue1", "mdKey2", "23")); - TbMsg msg = TbMsg.newMsg("POST_TELEMETRY", TenantId.SYS_TENANT_ID, metaData, data); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, metaData, data); String logMessage = node.toLogMessage(msg); log.info(logMessage); @@ -65,7 +66,7 @@ public class TbLogNodeTest { void givenEmptyDataMsg_whenToLog_thenReturnString() { TbLogNode node = new TbLogNode(); TbMsgMetaData metaData = new TbMsgMetaData(Collections.emptyMap()); - TbMsg msg = TbMsg.newMsg("POST_TELEMETRY", TenantId.SYS_TENANT_ID, metaData, ""); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, metaData, ""); String logMessage = node.toLogMessage(msg); log.info(logMessage); @@ -81,7 +82,7 @@ public class TbLogNodeTest { void givenNullDataMsg_whenToLog_thenReturnString() { TbLogNode node = new TbLogNode(); TbMsgMetaData metaData = new TbMsgMetaData(Collections.emptyMap()); - TbMsg msg = TbMsg.newMsg("POST_TELEMETRY", TenantId.SYS_TENANT_ID, metaData, null); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TenantId.SYS_TENANT_ID, metaData, null); String logMessage = node.toLogMessage(msg); log.info(logMessage); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java index 84deafe7e4..f44326a8d5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/edge/TbMsgPushToEdgeNodeTest.java @@ -35,12 +35,12 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; @@ -53,6 +53,9 @@ import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class TbMsgPushToEdgeNodeTest { + private static final List MISC_EVENTS = List.of(TbMsgType.CONNECT_EVENT, TbMsgType.DISCONNECT_EVENT, + TbMsgType.ACTIVITY_EVENT, TbMsgType.INACTIVITY_EVENT); + TbMsgPushToEdgeNode node; private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); @@ -81,8 +84,8 @@ public class TbMsgPushToEdgeNodeTest { Mockito.when(ctx.getEdgeService()).thenReturn(edgeService); Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, deviceId, new PageLink(TbMsgPushToEdgeNode.DEFAULT_PAGE_SIZE))).thenReturn(new PageData<>()); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), - TbMsgDataType.JSON, "{}", null, null); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, + TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, null, null); node.onMsg(ctx, msg); @@ -102,8 +105,8 @@ public class TbMsgPushToEdgeNodeTest { PageData edgePageData = new PageData<>(List.of(edgeId), 1, 1, false); Mockito.when(edgeService.findRelatedEdgeIdsByEntityId(tenantId, userId, new PageLink(TbMsgPushToEdgeNode.DEFAULT_PAGE_SIZE))).thenReturn(edgePageData); - TbMsg msg = TbMsg.newMsg(DataConstants.ATTRIBUTES_UPDATED, userId, new TbMsgMetaData(), - TbMsgDataType.JSON, "{}", null, null); + TbMsg msg = TbMsg.newMsg(TbMsgType.ATTRIBUTES_UPDATED, userId, TbMsgMetaData.EMPTY, + TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, null, null); node.onMsg(ctx, msg); @@ -112,9 +115,7 @@ public class TbMsgPushToEdgeNodeTest { @Test public void testMiscEventsProcessedAsAttributesUpdated() { - List miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT, - DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); - for (String event : miscEvents) { + for (var event : MISC_EVENTS) { TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue(DataConstants.SCOPE, DataConstants.SERVER_SCOPE); testEvent(event, metaData, EdgeEventActionType.ATTRIBUTES_UPDATED, "kv"); @@ -123,14 +124,12 @@ public class TbMsgPushToEdgeNodeTest { @Test public void testMiscEventsProcessedAsTimeseriesUpdated() { - List miscEvents = List.of(DataConstants.CONNECT_EVENT, DataConstants.DISCONNECT_EVENT, - DataConstants.ACTIVITY_EVENT, DataConstants.INACTIVITY_EVENT); - for (String event : miscEvents) { - testEvent(event, new TbMsgMetaData(), EdgeEventActionType.TIMESERIES_UPDATED, "data"); + for (var event : MISC_EVENTS) { + testEvent(event, TbMsgMetaData.EMPTY, EdgeEventActionType.TIMESERIES_UPDATED, "data"); } } - private void testEvent(String event, TbMsgMetaData metaData, EdgeEventActionType expectedType, String dataKey) { + private void testEvent(TbMsgType event, TbMsgMetaData metaData, EdgeEventActionType expectedType, String dataKey) { Mockito.when(ctx.getTenantId()).thenReturn(tenantId); Mockito.when(ctx.getEdgeService()).thenReturn(edgeService); Mockito.when(ctx.getEdgeEventService()).thenReturn(edgeEventService); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java index 4ddf75f29c..785be659fd 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbAssetTypeSwitchNodeTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -49,34 +50,30 @@ import static org.mockito.Mockito.when; class TbAssetTypeSwitchNodeTest { - TenantId tenantId; - AssetId assetId; - AssetId assetIdDeleted; - AssetProfile assetProfile; - TbContext ctx; - TbAssetTypeSwitchNode node; - EmptyNodeConfiguration config; - TbMsgCallback callback; - RuleEngineAssetProfileCache assetProfileCache; + private AssetId assetId; + private AssetId assetIdDeleted; + private TbContext ctx; + private TbAssetTypeSwitchNode node; + private TbMsgCallback callback; @BeforeEach void setUp() throws TbNodeException { - tenantId = new TenantId(UUID.randomUUID()); + TenantId tenantId = new TenantId(UUID.randomUUID()); assetId = new AssetId(UUID.randomUUID()); assetIdDeleted = new AssetId(UUID.randomUUID()); - assetProfile = new AssetProfile(); + AssetProfile assetProfile = new AssetProfile(); assetProfile.setTenantId(tenantId); assetProfile.setName("TestAssetProfile"); //node - config = new EmptyNodeConfiguration(); + EmptyNodeConfiguration config = new EmptyNodeConfiguration(); node = new TbAssetTypeSwitchNode(); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); //init mock ctx = mock(TbContext.class); - assetProfileCache = mock(RuleEngineAssetProfileCache.class); + RuleEngineAssetProfileCache assetProfileCache = mock(RuleEngineAssetProfileCache.class); callback = mock(TbMsgCallback.class); when(ctx.getTenantId()).thenReturn(tenantId); @@ -121,6 +118,7 @@ class TbAssetTypeSwitchNodeTest { } private TbMsg getTbMsg(EntityId entityId) { - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(), "{}", callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, callback); } + } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java new file mode 100644 index 0000000000..a146b95ee1 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNodeTest.java @@ -0,0 +1,165 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.RuleEngineAlarmService; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TbCheckAlarmStatusNodeTest { + + private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + private static final AlarmId ALARM_ID = new AlarmId(UUID.randomUUID()); + private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + + private TbCheckAlarmStatusNode node; + + private TbContext ctx; + private RuleEngineAlarmService alarmService; + + @BeforeEach + void setUp() throws TbNodeException { + var config = new TbCheckAlarmStatusNodeConfig().defaultConfiguration(); + + ctx = mock(TbContext.class); + alarmService = mock(RuleEngineAlarmService.class); + + when(ctx.getTenantId()).thenReturn(TENANT_ID); + when(ctx.getAlarmService()).thenReturn(alarmService); + when(ctx.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + + node = new TbCheckAlarmStatusNode(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenActiveAlarm_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var alarm = new Alarm(); + alarm.setId(ALARM_ID); + alarm.setOriginator(DEVICE_ID); + alarm.setType("General Alarm"); + + String msgData = JacksonUtil.toString(alarm); + TbMsg msg = getTbMsg(msgData); + + when(alarmService.findAlarmByIdAsync(TENANT_ID, ALARM_ID)).thenReturn(Futures.immediateFuture(alarm)); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenClearedAlarm_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var alarm = new Alarm(); + alarm.setId(ALARM_ID); + alarm.setOriginator(DEVICE_ID); + alarm.setType("General Alarm"); + alarm.setCleared(true); + + String msgData = JacksonUtil.toString(alarm); + TbMsg msg = getTbMsg(msgData); + + when(alarmService.findAlarmByIdAsync(TENANT_ID, ALARM_ID)).thenReturn(Futures.immediateFuture(alarm)); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenDeletedAlarm_whenOnMsg_then_Failure() throws TbNodeException { + // GIVEN + var alarm = new Alarm(); + alarm.setId(ALARM_ID); + alarm.setOriginator(DEVICE_ID); + alarm.setType("General Alarm"); + alarm.setCleared(true); + + String msgData = JacksonUtil.toString(alarm); + TbMsg msg = getTbMsg(msgData); + + when(alarmService.findAlarmByIdAsync(TENANT_ID, ALARM_ID)).thenReturn(Futures.immediateFuture(null)); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor throwableCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), throwableCaptor.capture()); + verify(ctx, never()).tellSuccess(any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + Throwable value = throwableCaptor.getValue(); + assertThat(value).isInstanceOf(TbNodeException.class).hasMessage("No such alarm found."); + } + + private TbMsg getTbMsg(String msgData) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, msgData); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeTest.java new file mode 100644 index 0000000000..e69cc55d7e --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckMessageNodeTest.java @@ -0,0 +1,202 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbCheckMessageNodeTest { + + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + private static final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + private TbCheckMessageNode node; + + private TbContext ctx; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbCheckMessageNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDefaultConfig_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithoutCheckAllKeysAndWithEmptyLists_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setCheckAllKeys(false); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckAllKeys_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0")); + configuration.setMetadataNames(List.of("deviceName", "deviceType", "ts")); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + @Test + void givenCustomConfigWithCheckAllKeys_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0", "temperature-1")); + configuration.setMetadataNames(List.of("deviceName", "deviceType", "ts")); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + @Test + void givenCustomConfigWithoutCheckAllKeys_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0", "temperature-1")); + configuration.setCheckAllKeys(false); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + @Test + void givenCustomConfigWithoutCheckAllKeysAndEmptyMsg_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var configuration = new TbCheckMessageNodeConfiguration().defaultConfiguration(); + configuration.setMessageNames(List.of("temperature-0", "temperature-1")); + configuration.setCheckAllKeys(false); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); + + TbMsg tbMsg = getTbMsg(true); + + // WHEN + node.onMsg(ctx, tbMsg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(tbMsg); + } + + private TbMsg getTbMsg() { + return getTbMsg(false); + } + + private TbMsg getTbMsg(boolean emptyData) { + String data = emptyData ? TbMsg.EMPTY_JSON_OBJECT : "{\"temperature-0\": 25}"; + var metadata = new TbMsgMetaData(); + metadata.putValue(DataConstants.DEVICE_NAME, "Test Device"); + metadata.putValue(DataConstants.DEVICE_TYPE, DataConstants.DEFAULT_DEVICE_TYPE); + metadata.putValue("ts", String.valueOf(System.currentTimeMillis())); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DEVICE_ID, metadata, data); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java new file mode 100644 index 0000000000..bf7fa31f18 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbCheckRelationNodeTest.java @@ -0,0 +1,313 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.util.TbPair; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.relation.RelationService; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TbCheckRelationNodeTest { + + private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); + private static final DeviceId ORIGINATOR_ID = new DeviceId(UUID.randomUUID()); + private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); + private static final TbMsg EMPTY_POST_ATTRIBUTES_MSG = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, ORIGINATOR_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + + private TbCheckRelationNode node; + + private TbContext ctx; + private RelationService relationService; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + relationService = mock(RelationService.class); + + when(ctx.getTenantId()).thenReturn(TENANT_ID); + when(ctx.getRelationService()).thenReturn(relationService); + when(ctx.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + + node = new TbCheckRelationNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDefaultConfig_whenInit_then_throwException() { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Entity should be specified!"); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntity_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + + when(relationService.checkRelationAsync(TENANT_ID, ORIGINATOR_ID, assetId, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(true)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntity_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + + when(relationService.checkRelationAsync(TENANT_ID, ORIGINATOR_ID, assetId, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(false)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntityAndDirectionTo_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + config.setDirection(EntitySearchDirection.TO.name()); + + when(relationService.checkRelationAsync(TENANT_ID, assetId, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(true)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigWithCheckRelationToSpecificEntityAndDirectionTo_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + + AssetId assetId = new AssetId(UUID.randomUUID()); + config.setEntityType(assetId.getEntityType().name()); + config.setEntityId(assetId.getId().toString()); + config.setDirection(EntitySearchDirection.TO.name()); + + when(relationService.checkRelationAsync(TENANT_ID, assetId, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(false)); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfig_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + var entityRelation = new EntityRelation(); + entityRelation.setFrom(ORIGINATOR_ID); + entityRelation.setTo(new AssetId(UUID.randomUUID())); + entityRelation.setType(EntityRelation.CONTAINS_TYPE); + entityRelation.setTypeGroup(RelationTypeGroup.COMMON); + + when(relationService.findByFromAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(List.of(entityRelation))); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByToAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfig_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + + when(relationService.findByFromAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(Collections.emptyList())); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByToAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigDirectionTo_whenOnMsg_then_True() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + config.setDirection(EntitySearchDirection.TO.name()); + var entityRelation = new EntityRelation(); + entityRelation.setFrom(new AssetId(UUID.randomUUID())); + entityRelation.setTo(ORIGINATOR_ID); + entityRelation.setType(EntityRelation.CONTAINS_TYPE); + entityRelation.setTypeGroup(RelationTypeGroup.COMMON); + + when(relationService.findByToAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(List.of(entityRelation))); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByFromAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenCustomConfigDirectionTo_whenOnMsg_then_False() throws TbNodeException { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setCheckForSingleEntity(false); + config.setDirection(EntitySearchDirection.TO.name()); + + when(relationService.findByToAndTypeAsync(TENANT_ID, ORIGINATOR_ID, config.getRelationType(), RelationTypeGroup.COMMON)).thenReturn(Futures.immediateFuture(Collections.emptyList())); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + // WHEN + node.onMsg(ctx, EMPTY_POST_ATTRIBUTES_MSG); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + verify(relationService, never()).findByFromAndTypeAsync(any(), any(), anyString(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(EMPTY_POST_ATTRIBUTES_MSG); + } + + @Test + void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + // GIVEN + var config = new TbCheckRelationNodeConfiguration().defaultConfiguration(); + config.setEntityType(ORIGINATOR_ID.getEntityType().name()); + config.setEntityId(ORIGINATOR_ID.getId().toString()); + String oldConfig = "{\"checkForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"" + config.getEntityType() + "\",\"entityId\":\"" + config.getEntityId() + "\",\"relationType\":\"Contains\"}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + // WHEN + TbPair upgrade = node.upgrade(0, configJson); + // THEN + assertTrue(upgrade.getFirst()); + assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass())); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java index e7b3ccd7f1..a6f4a6e3cf 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNodeTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -49,34 +50,30 @@ import static org.mockito.Mockito.when; class TbDeviceTypeSwitchNodeTest { - TenantId tenantId; - DeviceId deviceId; - DeviceId deviceIdDeleted; - DeviceProfile deviceProfile; - TbContext ctx; - TbDeviceTypeSwitchNode node; - EmptyNodeConfiguration config; - TbMsgCallback callback; - RuleEngineDeviceProfileCache deviceProfileCache; + private DeviceId deviceId; + private DeviceId deviceIdDeleted; + private TbContext ctx; + private TbDeviceTypeSwitchNode node; + private TbMsgCallback callback; @BeforeEach void setUp() throws TbNodeException { - tenantId = new TenantId(UUID.randomUUID()); + TenantId tenantId = new TenantId(UUID.randomUUID()); deviceId = new DeviceId(UUID.randomUUID()); deviceIdDeleted = new DeviceId(UUID.randomUUID()); - deviceProfile = new DeviceProfile(); + DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); deviceProfile.setName("TestDeviceProfile"); //node - config = new EmptyNodeConfiguration(); + EmptyNodeConfiguration config = new EmptyNodeConfiguration(); node = new TbDeviceTypeSwitchNode(); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); //init mock ctx = mock(TbContext.class); - deviceProfileCache = mock(RuleEngineDeviceProfileCache.class); + RuleEngineDeviceProfileCache deviceProfileCache = mock(RuleEngineDeviceProfileCache.class); callback = mock(TbMsgCallback.class); when(ctx.getTenantId()).thenReturn(tenantId); @@ -121,6 +118,6 @@ class TbDeviceTypeSwitchNodeTest { } private TbMsg getTbMsg(EntityId entityId) { - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(), "{}", callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index 4cfe38f3fc..639c1a7b1a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -29,6 +29,8 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -51,25 +53,25 @@ public class TbJsFilterNodeTest { @Mock private ScriptEngine scriptEngine; - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test - public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException { + public void falseEvaluationDoNotSendMsg() throws TbNodeException { initWithScript(); - TbMsg msg = TbMsg.newMsg("USER", null, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(false)); node.onMsg(ctx, msg); verify(ctx).getDbCallbackExecutor(); - verify(ctx).tellNext(msg, "False"); + verify(ctx).tellNext(msg, TbNodeConnectionType.FALSE); } @Test public void exceptionInJsThrowsException() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); - TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFailedFuture(new ScriptException("error"))); @@ -81,12 +83,12 @@ public class TbJsFilterNodeTest { public void metadataConditionCanBeTrue() throws TbNodeException { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); - TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); verify(ctx).getDbCallbackExecutor(); - verify(ctx).tellNext(msg, "True"); + verify(ctx).tellNext(msg, TbNodeConnectionType.TRUE); } private void initWithScript() throws TbNodeException { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java index f4f852c6bd..3343c5fb92 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -47,8 +48,8 @@ public class TbJsSwitchNodeTest { @Mock private ScriptEngine scriptEngine; - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test public void multipleRoutesAreAllowed() throws TbNodeException { @@ -58,7 +59,7 @@ public class TbJsSwitchNodeTest { metaData.putValue("humidity", "99"); String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(scriptEngine.executeSwitchAsync(msg)).thenReturn(Futures.immediateFuture(Sets.newHashSet("one", "three"))); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeTest.java new file mode 100644 index 0000000000..4e2e5dc430 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeTest.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; + +class TbMsgTypeFilterNodeTest { + + private DeviceId deviceId; + private TbContext ctx; + private TbMsgTypeFilterNode node; + + @BeforeEach + void setUp() throws TbNodeException { + ctx = mock(TbContext.class); + var config = new TbMsgTypeFilterNodeConfiguration().defaultConfiguration(); + deviceId = new DeviceId(UUID.randomUUID()); + node = new TbMsgTypeFilterNode(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenPostAttributes_whenOnMsg_then_True() { + // GIVEN + TbMsg msg = getTbMsg(deviceId, POST_ATTRIBUTES_REQUEST); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenAttributesUpdated_whenOnMsg_then_False() { + // GIVEN + TbMsg msg = getTbMsg(deviceId, ATTRIBUTES_UPDATED); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsg getTbMsg(EntityId entityId, TbMsgType msgType) { + return TbMsg.newMsg(msgType, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java new file mode 100644 index 0000000000..c4fc8cd76d --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNodeTest.java @@ -0,0 +1,94 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbMsgTypeSwitchNodeTest { + + private static final DeviceId DEVICE_ID = new DeviceId(UUID.randomUUID()); + + private TbMsgTypeSwitchNode node; + + private TbContext ctx; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbMsgTypeSwitchNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenAllTypes_whenOnMsg_then_allTypesSupported() throws TbNodeException { + // GIVEN + List tbMsgList = new ArrayList<>(); + var tbMsgTypes = TbMsgType.values(); + for (var msgType : tbMsgTypes) { + tbMsgList.add(getTbMsg(msgType)); + } + + // WHEN + for (TbMsg tbMsg : tbMsgList) { + node.onMsg(ctx, tbMsg); + } + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor nodeConnectionCapture = ArgumentCaptor.forClass(String.class); + verify(ctx, times(tbMsgList.size())).tellNext(newMsgCaptor.capture(), nodeConnectionCapture.capture()); + verify(ctx, never()).tellFailure(any(), any()); + var resultMsgs = newMsgCaptor.getAllValues(); + var resultNodeConnections = nodeConnectionCapture.getAllValues(); + for (int i = 0; i < resultMsgs.size(); i++) { + var msg = resultMsgs.get(i); + assertThat(msg).isNotNull(); + assertThat(msg.getType()).isNotNull(); + assertThat(msg).isSameAs(tbMsgList.get(i)); + assertThat(resultNodeConnections.get(i)) + .isEqualTo(TbMsgType.getRuleNodeConnectionOrElseOther(msg.getType())); + } + } + + private TbMsg getTbMsg(TbMsgType msgType) { + return TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeTest.java new file mode 100644 index 0000000000..3ed566b7e6 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNodeTest.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbOriginatorTypeFilterNodeTest { + + private TbContext ctx; + private TbOriginatorTypeFilterNode node; + + @BeforeEach + void setUp() throws TbNodeException { + ctx = mock(TbContext.class); + var config = new TbOriginatorTypeFilterNodeConfiguration().defaultConfiguration(); + node = new TbOriginatorTypeFilterNode(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenDevice_whenOnMsg_then_True() { + // GIVEN + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenAsset_whenOnMsg_then_False() { + // GIVEN + AssetId assetId = new AssetId(UUID.randomUUID()); + TbMsg msg = getTbMsg(assetId); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsg getTbMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNodeTest.java new file mode 100644 index 0000000000..796a7106fc --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNodeTest.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.filter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbOriginatorTypeSwitchNodeTest { + + private static final UUID RANDOM_UUID = UUID.randomUUID(); + + private TbOriginatorTypeSwitchNode node; + + private TbContext ctx; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbOriginatorTypeSwitchNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + @Test + void givenAllTypes_whenOnMsg_then_allTypesSupported() throws TbNodeException { + // GIVEN + List tbMsgList = new ArrayList<>(); + var entityTypes = EntityType.values(); + for (var entityType : entityTypes) { + var entityId = EntityIdFactory.getByTypeAndUuid(entityType, RANDOM_UUID); + tbMsgList.add(getTbMsg(entityId)); + } + + // WHEN + for (TbMsg tbMsg : tbMsgList) { + node.onMsg(ctx, tbMsg); + } + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor nodeConnectionCapture = ArgumentCaptor.forClass(String.class); + verify(ctx, times(tbMsgList.size())).tellNext(newMsgCaptor.capture(), nodeConnectionCapture.capture()); + verify(ctx, never()).tellFailure(any(), any()); + var resultMsgs = newMsgCaptor.getAllValues(); + var resultNodeConnections = nodeConnectionCapture.getAllValues(); + for (int i = 0; i < resultMsgs.size(); i++) { + var msg = resultMsgs.get(i); + assertThat(msg).isNotNull(); + assertThat(msg).isSameAs(tbMsgList.get(i)); + assertThat(resultNodeConnections.get(i)) + .isEqualTo(msg.getOriginator().getEntityType().getNormalName()); + } + } + + private TbMsg getTbMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGeoUtilTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GeoUtilTest.java similarity index 99% rename from rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGeoUtilTest.java rename to rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GeoUtilTest.java index 6a03a8f362..f53a876954 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGeoUtilTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/GeoUtilTest.java @@ -21,7 +21,7 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) -public class TbGeoUtilTest { +public class GeoUtilTest { public static final String SIMPLE_RECT = "[[51.903762928405555,23.642220786948297],[44.669801219635644,41.83345155830211]]"; public static final String SIMPLE_RECT_WITH_HOLE_IN_CENTER = "[[[44.66980121963565,23.642220786948297],[44.66980121963565,41.83345155830211],[51.903762928405555,41.83345155830211],[51.903762928405555,23.642220786948297]],[[46.10464044504632,26.234282119122227],[50.8755868028522,26.25625220459488],[51.04164771375101,38.5595000692786],[45.99790855491869,38.75723083853248]]]"; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeTest.java new file mode 100644 index 0000000000..f4585d0f96 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNodeTest.java @@ -0,0 +1,458 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.geo; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class TbGpsGeofencingFilterNodeTest { + + private static final double CIRCLE_RANGE = 1.0; + private static final Coordinates CIRCLE_CENTER = new Coordinates(49.0384, 31.4513); + private static final Coordinates POINT_INSIDE_CIRCLE = new Coordinates(49.0354, 31.4513); // distance from center: 0.334 km + private static final Coordinates POINT_OUTSIDE_CIRCLE = new Coordinates(49.0284, 31.4513); // distance from center: 1.112 km + + private TbContext ctx; + private TbGpsGeofencingFilterNode node; + + @BeforeEach + void setUp() { + ctx = mock(TbContext.class); + node = new TbGpsGeofencingFilterNode(); + } + + @AfterEach + void tearDown() { + node.destroy(); + } + + // Exception tests + + @Test + void givenDefaultConfig_whenOnMsg_thenExceptionInvalidMsg() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getEmptyArrayTbMsg(deviceId); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.onMsg(ctx, msg)); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Incoming Message is not a valid JSON object!"); + } + + @Test + void givenDefaultConfig_whenOnMsg_thenExceptionMissingPerimeterDefinitionNewVersion() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.onMsg(ctx, msg)); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Missing perimeter definition!"); + } + + @Test + void givenTypePolygonAndConfigWithoutPerimeterKeyName_whenOnMsg_thenExceptionMissingPerimeterDefinitionOldVersion() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + var exception = assertThrows(TbNodeException.class, () -> node.onMsg(ctx, msg)); + + // THEN + assertThat(exception.getMessage()).isEqualTo("Missing perimeter definition!"); + } + + // Polygon tests + + @Test + void givenTypePolygonAndConfigWithoutPerimeterKeyName_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypePolygonAndConfigWithoutPerimeterKeyName_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenDefaultConfig_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenDefaultConfig_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionPolygonPerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypePolygonAndConfigWithPolygonDefined_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPolygonsDefinition(GeoUtilTest.SIMPLE_RECT); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLatitude(), GeoUtilTest.POINT_INSIDE_SIMPLE_RECT_CENTER.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypePolygonAndConfigWithPolygonDefined_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPolygonsDefinition(GeoUtilTest.SIMPLE_RECT); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLatitude(), GeoUtilTest.POINT_OUTSIDE_SIMPLE_RECT.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsgMetaData getMetadataForOldVersionPolygonPerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("perimeter", GeoUtilTest.SIMPLE_RECT); + return metadata; + } + + private TbMsgMetaData getMetadataForNewVersionPolygonPerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("ss_perimeter", GeoUtilTest.SIMPLE_RECT); + return metadata; + } + + // Circle tests + + @Test + void givenTypeCircleAndConfigWithoutPerimeterKeyName_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_INSIDE_CIRCLE.getLatitude(), POINT_INSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircleAndConfigWithoutPerimeterKeyName_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterKeyName(null); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForOldVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_OUTSIDE_CIRCLE.getLatitude(), POINT_OUTSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircle_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterType(PerimeterType.CIRCLE); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_INSIDE_CIRCLE.getLatitude(), POINT_INSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircle_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setPerimeterType(PerimeterType.CIRCLE); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsgMetaData metadata = getMetadataForNewVersionCirclePerimeter(); + TbMsg msg = getTbMsg(deviceId, metadata, + POINT_OUTSIDE_CIRCLE.getLatitude(), POINT_OUTSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircleAndConfigWithCircleDefined_whenOnMsg_thenTrue() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPerimeterType(PerimeterType.CIRCLE); + config.setCenterLatitude(CIRCLE_CENTER.getLatitude()); + config.setCenterLongitude(CIRCLE_CENTER.getLongitude()); + config.setRange(CIRCLE_RANGE); + config.setRangeUnit(RangeUnit.KILOMETER); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + POINT_INSIDE_CIRCLE.getLatitude(), POINT_INSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.TRUE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + @Test + void givenTypeCircleAndConfigWithCircleDefined_whenOnMsg_thenFalse() throws TbNodeException { + // GIVEN + var config = new TbGpsGeofencingFilterNodeConfiguration().defaultConfiguration(); + config.setFetchPerimeterInfoFromMessageMetadata(false); + config.setPerimeterType(PerimeterType.CIRCLE); + config.setCenterLatitude(CIRCLE_CENTER.getLatitude()); + config.setCenterLongitude(CIRCLE_CENTER.getLongitude()); + config.setRange(CIRCLE_RANGE); + config.setRangeUnit(RangeUnit.KILOMETER); + node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + TbMsg msg = getTbMsg(deviceId, TbMsgMetaData.EMPTY, + POINT_OUTSIDE_CIRCLE.getLatitude(), POINT_OUTSIDE_CIRCLE.getLongitude()); + + // WHEN + node.onMsg(ctx, msg); + + // THEN + ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, times(1)).tellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.FALSE)); + verify(ctx, never()).tellFailure(any(), any()); + TbMsg newMsg = newMsgCaptor.getValue(); + assertThat(newMsg).isNotNull(); + assertThat(newMsg).isSameAs(msg); + } + + private TbMsgMetaData getMetadataForOldVersionCirclePerimeter() { + var metadata = new TbMsgMetaData(); + metadata.putValue("centerLatitude", String.valueOf(CIRCLE_CENTER.getLatitude())); + metadata.putValue("centerLongitude", String.valueOf(CIRCLE_CENTER.getLongitude())); + metadata.putValue("range", String.valueOf(CIRCLE_RANGE)); + metadata.putValue("rangeUnit", String.valueOf(RangeUnit.KILOMETER)); + return metadata; + } + + private TbMsgMetaData getMetadataForNewVersionCirclePerimeter() { + ObjectNode perimeter = JacksonUtil.newObjectNode(); + perimeter.put("latitude", CIRCLE_CENTER.getLatitude()); + perimeter.put("longitude", CIRCLE_CENTER.getLongitude()); + perimeter.put("radius", CIRCLE_RANGE); + perimeter.put("radiusUnit", String.valueOf(RangeUnit.KILOMETER)); + var metadata = new TbMsgMetaData(); + metadata.putValue("ss_perimeter", JacksonUtil.toString(perimeter)); + return metadata; + } + + private TbMsg getTbMsg(EntityId entityId, TbMsgMetaData metadata, double latitude, double longitude) { + String data = "{\"latitude\": " + latitude + ", \"longitude\": " + longitude + "}"; + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metadata, data); + } + + private TbMsg getEmptyArrayTbMsg(EntityId entityId) { + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index c95183717f..bf4d090c1c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -56,26 +57,26 @@ public class TbMsgToEmailNodeTest { private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); @Test - public void msgCanBeConverted() throws IOException { + public void msgCanBeConverted() { initWithScript(); metaData.putValue("username", "oreo"); metaData.putValue("userEmail", "user@email.io"); metaData.putValue("name", "temp"); metaData.putValue("passed", "5"); metaData.putValue("count", "100"); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); emailNode.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("SEND_EMAIL", typeCaptor.getValue()); + assertEquals(TbMsgType.SEND_EMAIL, typeCaptor.getValue()); assertEquals(originator, originatorCaptor.getValue()); assertEquals("oreo", metadataCaptor.getValue().getValue("username")); assertNotSame(metaData, metadataCaptor.getValue()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 2efc438f8f..0a42349433 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; @@ -149,7 +150,7 @@ public class TbMathNodeTest { metaData.putValue("key2", "argumentA"); ObjectNode msgNode = JacksonUtil.newObjectNode() .put("key3", "argumentB").put("argumentA", 2).put("argumentB", 2); - TbMsg msg = TbMsg.newMsg("TEST", originator, metaData, msgNode.toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, msgNode.toString()); node.onMsg(ctx, msg); @@ -162,7 +163,7 @@ public class TbMathNodeTest { metaData.putValue("key2", "argumentC"); msgNode = JacksonUtil.newObjectNode() .put("key3", "argumentD").put("argumentC", 4).put("argumentD", 3); - msg = TbMsg.newMsg("TEST", originator, metaData, msgNode.toString()); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, msgNode.toString()); node.onMsg(ctx, msg); @@ -246,7 +247,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).put("b", arg2).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", arg1).put("b", arg2).toString()); node.onMsg(ctx, msg); @@ -269,7 +270,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", arg1).toString()); node.onMsg(ctx, msg); @@ -292,7 +293,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); @@ -315,7 +316,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); @@ -339,7 +340,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.TIME_SERIES, "b") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().toString()); Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, "a")) .thenReturn(Futures.immediateFuture(Optional.of(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("a", 2.0))))); @@ -367,7 +368,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); node.onMsg(ctx, msg); @@ -389,7 +390,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); node.onMsg(ctx, msg); @@ -411,7 +412,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); Mockito.when(telemetryService.saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble())) .thenReturn(Futures.immediateFuture(null)); @@ -437,7 +438,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); Mockito.when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) .thenReturn(Futures.immediateFuture(null)); @@ -462,7 +463,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 5).toString()); Mockito.when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) .thenReturn(Futures.immediateFuture(null)); @@ -493,7 +494,7 @@ public class TbMathNodeTest { new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 3, false, false, null), tbMathArgument ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 10).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 10).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -513,7 +514,7 @@ public class TbMathNodeTest { new TbMathResult(TbMathArgumentType.TIME_SERIES, "result", 3, true, false, DataConstants.SERVER_SCOPE), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 10).toString()); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, JacksonUtil.newObjectNode().put("a", 10).toString()); Throwable thrown = assertThrows(RuntimeException.class, () -> { node.onMsg(ctx, msg); }); @@ -527,7 +528,7 @@ public class TbMathNodeTest { new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); - TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), "[]"); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); Throwable thrown = assertThrows(RuntimeException.class, () -> { node.onMsg(ctx, msg); }); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java index 86765446e0..0fd8793e82 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java @@ -16,10 +16,8 @@ package org.thingsboard.rule.engine.metadata; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import org.assertj.core.api.Assertions; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +27,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -41,13 +40,14 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.List; import java.util.UUID; -import java.util.concurrent.Callable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -70,21 +70,7 @@ public class CalculateDeltaNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -117,13 +103,13 @@ public class CalculateDeltaNodeTest { public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() { // GIVEN var msgData = "{\"pulseCounter\": 42}"; - var msg = TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); + verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -131,14 +117,13 @@ public class CalculateDeltaNodeTest { @Test public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() { // GIVEN - var msgData = "[]"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); + verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -147,13 +132,13 @@ public class CalculateDeltaNodeTest { @Test public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() { // GIVEN - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "{}"); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); // WHEN node.onMsg(ctxMock, msg); // THEN - verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other")); + verify(ctxMock, times(1)).tellNext(eq(msg), eq(TbNodeConnectionType.OTHER)); verify(ctxMock, never()).tellSuccess(any()); verify(ctxMock, never()).tellFailure(any(), any()); } @@ -171,7 +156,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 40.5))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -201,7 +186,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 40L))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -231,7 +216,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("temperature", "40.0"))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -265,7 +250,7 @@ public class CalculateDeltaNodeTest { var msgData = "{\"temperature\": 42,\"airPressure\":123}"; var firstMsgMetaData = new TbMsgMetaData(); firstMsgMetaData.putValue("ts", String.valueOf(3L)); - var firstMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, firstMsgMetaData, msgData); + var firstMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, firstMsgMetaData, msgData); // WHEN node.onMsg(ctxMock, firstMsg); @@ -289,7 +274,7 @@ public class CalculateDeltaNodeTest { var secondMsgMetaData = new TbMsgMetaData(); secondMsgMetaData.putValue("ts", String.valueOf(6L)); - var secondMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData); + var secondMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData); // WHEN node.onMsg(ctxMock, secondMsg); @@ -320,7 +305,7 @@ public class CalculateDeltaNodeTest { mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", null))); var msgData = "{\"temperature\": 42,\"airPressure\":123}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -348,7 +333,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); var msgData = "{\"pulseCounter\":\"123\"}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN node.onMsg(ctxMock, msg); @@ -368,7 +353,33 @@ public class CalculateDeltaNodeTest { assertEquals(msg, actualMsgCaptor.getValue()); assertInstanceOf(IllegalArgumentException.class, actualException); assertEquals(expectedExceptionMsg, actualException.getMessage()); + } + + @Test + public void givenNegativeDeltaAndTellFailureIfNegativeDeltaFalse_whenOnMsg_thenShouldTellSuccess() throws TbNodeException { + // GIVEN + config.setTellFailureIfDeltaIsNegative(false); + nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + node.init(ctxMock, nodeConfiguration); + + mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L))); + var msgData = "{\"pulseCounter\":\"123\"}"; + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + + verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture()); + verify(ctxMock, never()).tellFailure(any(), any()); + verify(ctxMock, never()).tellNext(any(), anyString()); + verify(ctxMock, never()).tellNext(any(), anySet()); + + String expectedMsgData = "{\"pulseCounter\":\"123\",\"delta\":-77}"; + assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData()); } @Test @@ -377,7 +388,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high"))); var msgData = "{\"pulseCounter\":\"123\"}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN-THEN Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) @@ -391,7 +402,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false))); var msgData = "{\"pulseCounter\":true}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN-THEN Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) @@ -405,7 +416,7 @@ public class CalculateDeltaNodeTest { mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}"))); var msgData = "{\"pulseCounter\":{\"isActive\":true}}"; - var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, msgData); // WHEN-THEN Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java index 4c9d138236..c5044eab37 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java @@ -28,10 +28,13 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -51,7 +54,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.thingsboard.server.common.data.security.DeviceCredentialsType.ACCESS_TOKEN; @ExtendWith(MockitoExtension.class) public class TbFetchDeviceCredentialsNodeTest { @@ -82,13 +84,13 @@ public class TbFetchDeviceCredentialsNodeTest { @Test void givenDefaultConfig_whenInit_thenOK() { assertThat(node.config).isEqualTo(config); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test void givenDefaultConfig_whenVerify_thenOK() { var defaultConfig = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration(); - assertThat(defaultConfig.getFetchTo()).isEqualTo(FetchTo.METADATA); + assertThat(defaultConfig.getFetchTo()).isEqualTo(TbMsgSource.METADATA); } @Test @@ -97,7 +99,7 @@ public class TbFetchDeviceCredentialsNodeTest { doReturn(deviceCredentialsServiceMock).when(ctxMock).getDeviceCredentialsService(); doAnswer(invocation -> { DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentials.setCredentialsType(ACCESS_TOKEN); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); return deviceCredentials; }).when(deviceCredentialsServiceMock).findDeviceCredentialsByDeviceId(any(), any()); doAnswer(invocation -> JacksonUtil.newObjectNode()).when(deviceCredentialsServiceMock).toCredentialsInfo(any()); @@ -171,7 +173,7 @@ public class TbFetchDeviceCredentialsNodeTest { final var metaData = new TbMsgMetaData(mdMap); final String data = "{\"TestAttribute_1\": \"humidity\", \"TestAttribute_2\": \"voltage\"}"; - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, metaData, data, callbackMock); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, metaData, data, callbackMock); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java index 5146f2f1e9..6c8e83a22f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java @@ -31,6 +31,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -125,7 +127,7 @@ public class TbGetAttributesNodeTest { @Test public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.METADATA, false, false); + node = initNode(TbMsgSource.METADATA, false, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -134,17 +136,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.METADATA, false, tsKeys); + checkTs(resultMsg, TbMsgSource.METADATA, false, tsKeys); } @Test public void givenFetchLatestTimeseriesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.METADATA, true, false); + node = initNode(TbMsgSource.METADATA, true, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -153,17 +155,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.METADATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.METADATA, true, tsKeys); + checkTs(resultMsg, TbMsgSource.METADATA, true, tsKeys); } @Test public void givenFetchAttributesToData_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, false, false); + node = initNode(TbMsgSource.DATA, false, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -172,17 +174,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.DATA, false, tsKeys); + checkTs(resultMsg, TbMsgSource.DATA, false, tsKeys); } @Test public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellSuccess() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, true, false); + node = initNode(TbMsgSource.DATA, true, false); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -191,17 +193,17 @@ public class TbGetAttributesNodeTest { // THEN var resultMsg = checkMsg(true); - checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes); - checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "cs_", clientAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "ss_", serverAttributes); + checkAttributes(resultMsg, TbMsgSource.DATA, "shared_", sharedAttributes); - checkTs(resultMsg, FetchTo.DATA, true, tsKeys); + checkTs(resultMsg, TbMsgSource.DATA, true, tsKeys); } @Test public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellFailure() throws Exception { // GIVEN - node = initNode(FetchTo.METADATA, false, true); + node = initNode(TbMsgSource.METADATA, false, true); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -210,17 +212,17 @@ public class TbGetAttributesNodeTest { // THEN var actualMsg = checkMsg(false); - checkAttributes(actualMsg, FetchTo.METADATA, "cs_", clientAttributes); - checkAttributes(actualMsg, FetchTo.METADATA, "ss_", serverAttributes); - checkAttributes(actualMsg, FetchTo.METADATA, "shared_", sharedAttributes); + checkAttributes(actualMsg, TbMsgSource.METADATA, "cs_", clientAttributes); + checkAttributes(actualMsg, TbMsgSource.METADATA, "ss_", serverAttributes); + checkAttributes(actualMsg, TbMsgSource.METADATA, "shared_", sharedAttributes); - checkTs(actualMsg, FetchTo.METADATA, false, tsKeys); + checkTs(actualMsg, TbMsgSource.METADATA, false, tsKeys); } @Test public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellFailure() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, true, true); + node = initNode(TbMsgSource.DATA, true, true); var msg = getTbMsg(ORIGINATOR); // WHEN @@ -229,18 +231,18 @@ public class TbGetAttributesNodeTest { // THEN var actualMsg = checkMsg(false); - checkAttributes(actualMsg, FetchTo.DATA, "cs_", clientAttributes); - checkAttributes(actualMsg, FetchTo.DATA, "ss_", serverAttributes); - checkAttributes(actualMsg, FetchTo.DATA, "shared_", sharedAttributes); + checkAttributes(actualMsg, TbMsgSource.DATA, "cs_", clientAttributes); + checkAttributes(actualMsg, TbMsgSource.DATA, "ss_", serverAttributes); + checkAttributes(actualMsg, TbMsgSource.DATA, "shared_", sharedAttributes); - checkTs(actualMsg, FetchTo.DATA, true, tsKeys); + checkTs(actualMsg, TbMsgSource.DATA, true, tsKeys); } @Test public void givenFetchLatestTimeseriesToDataAndDataIsNotJsonObject_whenOnMsg_thenException() throws Exception { // GIVEN - node = initNode(FetchTo.DATA, true, true); - var msg = TbMsg.newMsg("TEST", ORIGINATOR, new TbMsgMetaData(), "[]"); + node = initNode(TbMsgSource.DATA, true, true); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -267,6 +269,22 @@ public class TbGetAttributesNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } + @Test + public void givenOldConfigWithNoFetchToDataProperty_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + var defaultConfig = new TbGetAttributesNodeConfiguration().defaultConfiguration(); + var node = new TbGetAttributesNode(); + String oldConfig = "{\"clientAttributeNames\":[]," + + "\"sharedAttributeNames\":[]," + + "\"serverAttributeNames\":[]," + + "\"latestTsKeyNames\":[]," + + "\"tellFailureIfAbsent\":true," + + "\"getLatestValueWithTs\":false}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + TbPair upgrade = node.upgrade(0, configJson); + Assertions.assertTrue(upgrade.getFirst()); + Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); + } + private TbMsg checkMsg(boolean checkSuccess) { var msgCaptor = ArgumentCaptor.forClass(TbMsg.class); if (checkSuccess) { @@ -288,15 +306,15 @@ public class TbGetAttributesNodeTest { return resultMsg; } - private void checkAttributes(TbMsg actualMsg, FetchTo fetchTo, String prefix, List attributes) { + private void checkAttributes(TbMsg actualMsg, TbMsgSource fetchTo, String prefix, List attributes) { var msgData = JacksonUtil.toJsonNode(actualMsg.getData()); attributes.stream() .filter(attribute -> !attribute.equals("unknown")) .forEach(attribute -> { String result = null; - if (FetchTo.DATA.equals(fetchTo)) { + if (TbMsgSource.DATA.equals(fetchTo)) { result = msgData.get(prefix + attribute).asText(); - } else if (FetchTo.METADATA.equals(fetchTo)) { + } else if (TbMsgSource.METADATA.equals(fetchTo)) { result = actualMsg.getMetaData().getValue(prefix + attribute); } assertNotNull(result); @@ -304,7 +322,7 @@ public class TbGetAttributesNodeTest { }); } - private void checkTs(TbMsg actualMsg, FetchTo fetchTo, boolean getLatestValueWithTs, List tsKeys) { + private void checkTs(TbMsg actualMsg, TbMsgSource fetchTo, boolean getLatestValueWithTs, List tsKeys) { var msgData = JacksonUtil.toJsonNode(actualMsg.getData()); long value = 1L; for (var key : tsKeys) { @@ -318,9 +336,9 @@ public class TbGetAttributesNodeTest { } else { expectedValue = "{\"data\":" + value + "}"; } - if (FetchTo.DATA.equals(fetchTo)) { + if (TbMsgSource.DATA.equals(fetchTo)) { actualValue = JacksonUtil.toString(msgData.get(key)); - } else if (FetchTo.METADATA.equals(fetchTo)) { + } else if (TbMsgSource.METADATA.equals(fetchTo)) { actualValue = actualMsg.getMetaData().getValue(key); } assertNotNull(actualValue); @@ -329,7 +347,7 @@ public class TbGetAttributesNodeTest { } } - private TbGetAttributesNode initNode(FetchTo fetchTo, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException { + private TbGetAttributesNode initNode(TbMsgSource fetchTo, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException { var config = new TbGetAttributesNodeConfiguration(); config.setClientAttributeNames(List.of("client_attr_1", "client_attr_2", "${client_attr_metadata}", "unknown")); config.setServerAttributeNames(List.of("server_attr_1", "server_attr_2", "${server_attr_metadata}", "unknown")); @@ -353,7 +371,7 @@ public class TbGetAttributesNodeTest { msgMetaData.putValue("client_attr_metadata", "client_attr_3"); msgMetaData.putValue("server_attr_metadata", "server_attr_3"); - return TbMsg.newMsg("TEST", entityId, msgMetaData, JacksonUtil.toString(msgData)); + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, msgMetaData, JacksonUtil.toString(msgData)); } private List getAttributeNames(String prefix) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java index 156c3f03d7..291614464a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java @@ -17,9 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,10 +28,13 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; @@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -62,7 +64,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -76,7 +77,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @ExtendWith(MockitoExtension.class) public class TbGetCustomerAttributeNodeTest { @@ -84,21 +84,7 @@ public class TbGetCustomerAttributeNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -176,7 +162,7 @@ public class TbGetCustomerAttributeNodeTest { assertThat(node.config).isEqualTo(config); assertThat(config.getDataMapping()).isEqualTo(Map.of("alarmThreshold", "threshold")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test @@ -187,7 +173,7 @@ public class TbGetCustomerAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); config.setDataToFetch(DataToFetch.LATEST_TELEMETRY); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -200,7 +186,7 @@ public class TbGetCustomerAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); } @Test @@ -222,8 +208,8 @@ public class TbGetCustomerAttributeNodeTest { @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -238,7 +224,7 @@ public class TbGetCustomerAttributeNodeTest { // GIVEN var userId = new UserId(UUID.randomUUID()); - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", userId, new TbMsgMetaData(), "{}"); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, userId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -276,7 +262,7 @@ public class TbGetCustomerAttributeNodeTest { var device = new Device(new DeviceId(UUID.randomUUID())); device.setCustomerId(CUSTOMER_ID); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.ATTRIBUTES, device.getId()); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -291,7 +277,7 @@ public class TbGetCustomerAttributeNodeTest { doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId())); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -323,7 +309,7 @@ public class TbGetCustomerAttributeNodeTest { var user = new User(new UserId(UUID.randomUUID())); user.setCustomerId(CUSTOMER_ID); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, user.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, user.getId()); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -338,7 +324,7 @@ public class TbGetCustomerAttributeNodeTest { doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId())); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -369,7 +355,7 @@ public class TbGetCustomerAttributeNodeTest { // GIVEN var customer = new Customer(new CustomerId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, customer.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.LATEST_TELEMETRY, customer.getId()); List timeseriesList = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -413,7 +399,7 @@ public class TbGetCustomerAttributeNodeTest { var asset = new Asset(new AssetId(UUID.randomUUID())); asset.setCustomerId(new CustomerId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId()); List timeseriesList = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -465,7 +451,7 @@ public class TbGetCustomerAttributeNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, DataToFetch dataToFetch, EntityId originator) { config.setDataMapping(Map.of( "sourceKey1", "targetKey1", "${metaDataPattern1}", "$[messageBodyPattern1]", @@ -482,7 +468,7 @@ public class TbGetCustomerAttributeNodeTest { var msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } @RequiredArgsConstructor diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java index 2830dddaeb..51625eafe7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java @@ -17,8 +17,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,10 +26,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -60,7 +61,6 @@ import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -76,21 +76,7 @@ public class TbGetCustomerDetailsNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -148,14 +134,14 @@ public class TbGetCustomerDetailsNodeTest { @Test public void givenDefaultConfig_whenInit_thenOK() { assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList()); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.DATA); } @Test public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { // GIVEN config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -164,15 +150,15 @@ public class TbGetCustomerDetailsNodeTest { // THEN assertThat(node.config).isEqualTo(config); assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -189,7 +175,7 @@ public class TbGetCustomerDetailsNodeTest { device.setId(new DeviceId(UUID.randomUUID())); device.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values()), device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.values()), device.getId()); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); @@ -231,7 +217,7 @@ public class TbGetCustomerDetailsNodeTest { asset.setId(new AssetId(UUID.randomUUID())); asset.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE), asset.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE), asset.getId()); when(ctxMock.getAssetService()).thenReturn(assetServiceMock); when(assetServiceMock.findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()))).thenReturn(Futures.immediateFuture(asset)); @@ -269,7 +255,7 @@ public class TbGetCustomerDetailsNodeTest { user.setId(new UserId(UUID.randomUUID())); user.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), user.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), user.getId()); when(ctxMock.getUserService()).thenReturn(userServiceMock); when(userServiceMock.findUserByIdAsync(eq(TENANT_ID), eq(user.getId()))).thenReturn(Futures.immediateFuture(user)); @@ -298,7 +284,7 @@ public class TbGetCustomerDetailsNodeTest { edge.setId(new EdgeId(UUID.randomUUID())); edge.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -330,7 +316,7 @@ public class TbGetCustomerDetailsNodeTest { edge.setId(new EdgeId(UUID.randomUUID())); edge.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId()); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -359,7 +345,7 @@ public class TbGetCustomerDetailsNodeTest { device.setId(new DeviceId(UUID.randomUUID())); device.setName("Thermostat"); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), device.getId()); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -397,7 +383,7 @@ public class TbGetCustomerDetailsNodeTest { device.setId(new DeviceId(UUID.randomUUID())); device.setCustomerId(customer.getId()); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO), device.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO), device.getId()); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); @@ -425,7 +411,7 @@ public class TbGetCustomerDetailsNodeTest { var dashboard = new Dashboard(); dashboard.setId(new DashboardId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.STATE), dashboard.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, List.of(ContactBasedEntityDetails.STATE), dashboard.getId()); // WHEN node.onMsg(ctxMock, msg); @@ -458,7 +444,7 @@ public class TbGetCustomerDetailsNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, List detailsList, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, List detailsList, EntityId originator) { config.setDetailsList(detailsList); config.setFetchTo(fetchTo); @@ -471,7 +457,7 @@ public class TbGetCustomerDetailsNodeTest { var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } private void mockFindCustomer() { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java index 82e24226f5..bdb431227b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java @@ -42,4 +42,22 @@ public class TbGetDeviceAttrNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } + @Test + public void givenOldConfigWithNoFetchToDataProperty_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + var defaultConfig = new TbGetDeviceAttrNodeConfiguration().defaultConfiguration(); + var node = new TbGetDeviceAttrNode(); + String oldConfig = "{\"clientAttributeNames\":[]," + + "\"sharedAttributeNames\":[]," + + "\"serverAttributeNames\":[]," + + "\"latestTsKeyNames\":[]," + + "\"tellFailureIfAbsent\":true," + + "\"getLatestValueWithTs\":false," + + "\"deviceRelationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1,\"relationType\":\"Contains\",\"deviceTypes\":[\"default\"]," + + "\"fetchLastLevelOnly\":false}}"; + JsonNode configJson = JacksonUtil.toJsonNode(oldConfig); + TbPair upgrade = node.upgrade(0, configJson); + Assertions.assertTrue(upgrade.getFirst()); + Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); + } + } \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java index 4f03fe54c0..d681deb9c0 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java @@ -16,9 +16,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,13 +25,16 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -43,7 +43,6 @@ import org.thingsboard.server.dao.device.DeviceService; import java.util.Collections; import java.util.Map; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; @@ -60,21 +59,7 @@ public class TbGetOriginatorFieldsNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId DUMMY_TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -116,8 +101,8 @@ public class TbGetOriginatorFieldsNodeTest { "name", "originatorName", "type", "originatorType")); assertThat(config.isIgnoreNullStrings()).isEqualTo(false); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test @@ -128,7 +113,7 @@ public class TbGetOriginatorFieldsNodeTest { "title", "originatorTitle", "country", "originatorCountry")); config.setIgnoreNullStrings(true); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -141,15 +126,15 @@ public class TbGetOriginatorFieldsNodeTest { "title", "originatorTitle", "country", "originatorCountry")); assertThat(config.isIgnoreNullStrings()).isEqualTo(true); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); } @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -172,13 +157,55 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(true); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); node.config = config; - node.fetchTo = FetchTo.DATA; + node.fetchTo = TbMsgSource.DATA; var msgMetaData = new TbMsgMetaData(); var msgData = "{\"temp\":42,\"humidity\":77}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + + when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); + when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); + when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device); + + when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); + + // WHEN + node.onMsg(ctxMock, msg); + + // THEN + var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture()); + verify(ctxMock, never()).tellFailure(any(), any()); + + var expectedMsgData = "{\"temp\":42,\"humidity\":77,\"originatorName\":\"Test device\",\"originatorType\":\"Test device type\"}"; + + assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData); + assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msgMetaData); + } + + @Test + public void givenDeviceWithEmptyLabel_whenOnMsg_thenShouldTellSuccessAndFetchToData() throws TbNodeException, ExecutionException, InterruptedException { + // GIVEN + var device = new Device(); + device.setId(DUMMY_DEVICE_ORIGINATOR); + device.setName("Test device"); + device.setType("Test device type"); + device.setLabel(""); + + config.setDataMapping(Map.of( + "name", "originatorName", + "type", "originatorType", + "label", "originatorLabel")); + config.setIgnoreNullStrings(true); + config.setFetchTo(TbMsgSource.DATA); + + node.config = config; + node.fetchTo = TbMsgSource.DATA; + var msgMetaData = new TbMsgMetaData(); + var msgData = "{\"temp\":42,\"humidity\":77}"; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); @@ -213,15 +240,15 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(true); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); node.config = config; - node.fetchTo = FetchTo.METADATA; + node.fetchTo = TbMsgSource.METADATA; var msgMetaData = new TbMsgMetaData(Map.of( "testKey1", "testValue1", "testKey2", "123")); var msgData = "[\"value1\",\"value2\"]"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); @@ -261,15 +288,15 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(false); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); node.config = config; - node.fetchTo = FetchTo.METADATA; + node.fetchTo = TbMsgSource.METADATA; var msgMetaData = new TbMsgMetaData(Map.of( "testKey1", "testValue1", "testKey2", "123")); var msgData = "[\"value1\",\"value2\"]"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID); @@ -319,15 +346,15 @@ public class TbGetOriginatorFieldsNodeTest { "type", "originatorType", "label", "originatorLabel")); config.setIgnoreNullStrings(false); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); node.config = config; - node.fetchTo = FetchTo.METADATA; + node.fetchTo = TbMsgSource.METADATA; var msgMetaData = new TbMsgMetaData(Map.of( "testKey1", "testValue1", "testKey2", "123")); var msgData = "[\"value1\",\"value2\"]"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", new DashboardId(UUID.randomUUID()), msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, new DashboardId(UUID.randomUUID()), msgMetaData, msgData); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java index 23ceb082bc..dcf5c22ea5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java @@ -17,9 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,12 +28,15 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.data.RelationsQuery; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.Tenant; @@ -54,13 +55,13 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; @@ -72,7 +73,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -86,28 +86,13 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @ExtendWith(MockitoExtension.class) public class TbGetRelatedAttributeNodeTest { private static final EntityId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -172,7 +157,7 @@ public class TbGetRelatedAttributeNodeTest { assertThat(nodeConfig).isEqualTo(config); assertThat(nodeConfig.getDataMapping()).isEqualTo(Map.of("serialNumber", "sn")); assertThat(nodeConfig.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); var relationsQuery = new RelationsQuery(); var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); @@ -191,7 +176,7 @@ public class TbGetRelatedAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); config.setDataToFetch(DataToFetch.LATEST_TELEMETRY); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); var relationsQuery = new RelationsQuery(); var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); @@ -214,7 +199,7 @@ public class TbGetRelatedAttributeNodeTest { "sourceAttr3", "targetKey3" )); assertThat(nodeConfig.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); assertThat(nodeConfig.getRelationsQuery()).isEqualTo(relationsQuery); } @@ -237,8 +222,8 @@ public class TbGetRelatedAttributeNodeTest { @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -251,7 +236,7 @@ public class TbGetRelatedAttributeNodeTest { @Test public void givenDidNotFindEntity_whenOnMsg_thenShouldTellFailure() { // GIVEN - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, DUMMY_DEVICE_ORIGINATOR); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, DUMMY_DEVICE_ORIGINATOR); when(ctxMock.getTenantId()).thenReturn(TENANT_ID); @@ -287,7 +272,7 @@ public class TbGetRelatedAttributeNodeTest { var customer = new Customer(new CustomerId(UUID.randomUUID())); var user = new User(new UserId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, customer.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.ATTRIBUTES, customer.getId()); entityRelation.setFrom(customer.getId()); entityRelation.setTo(user.getId()); @@ -306,7 +291,7 @@ public class TbGetRelatedAttributeNodeTest { doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any()); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(user.getId()), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(user.getId()), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributes)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -338,7 +323,7 @@ public class TbGetRelatedAttributeNodeTest { var firstCustomer = new Customer(new CustomerId(UUID.randomUUID())); var secondCustomer = new Customer(new CustomerId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, firstCustomer.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, firstCustomer.getId()); entityRelation.setFrom(firstCustomer.getId()); entityRelation.setTo(secondCustomer.getId()); @@ -357,7 +342,7 @@ public class TbGetRelatedAttributeNodeTest { doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any()); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(secondCustomer.getId()), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(secondCustomer.getId()), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributes)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -389,7 +374,7 @@ public class TbGetRelatedAttributeNodeTest { var dashboard = new Dashboard(new DashboardId(UUID.randomUUID())); var entityView = new EntityView(new EntityViewId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, dashboard.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.LATEST_TELEMETRY, dashboard.getId()); entityRelation.setFrom(dashboard.getId()); entityRelation.setTo(entityView.getId()); @@ -440,7 +425,7 @@ public class TbGetRelatedAttributeNodeTest { var tenant = new Tenant(new TenantId(UUID.randomUUID())); var device = new Device(new DeviceId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, tenant.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, tenant.getId()); entityRelation.setFrom(tenant.getId()); entityRelation.setTo(device.getId()); @@ -493,7 +478,7 @@ public class TbGetRelatedAttributeNodeTest { device.setName("Device Name"); var asset = new Asset(new AssetId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.FIELDS, asset.getId()); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.FIELDS, asset.getId()); entityRelation.setFrom(asset.getId()); entityRelation.setTo(device.getId()); @@ -533,7 +518,7 @@ public class TbGetRelatedAttributeNodeTest { device.setName("Device Name"); var asset = new Asset(new AssetId(UUID.randomUUID())); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.FIELDS, asset.getId()); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.FIELDS, asset.getId()); entityRelation.setFrom(asset.getId()); entityRelation.setTo(device.getId()); @@ -583,7 +568,7 @@ public class TbGetRelatedAttributeNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, DataToFetch dataToFetch, EntityId originator) { config.setDataToFetch(dataToFetch); config.setFetchTo(fetchTo); @@ -607,7 +592,7 @@ public class TbGetRelatedAttributeNodeTest { msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}"; } - msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } @RequiredArgsConstructor diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java index d8532247ae..87b34cffe0 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java @@ -17,9 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,10 +27,12 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.util.TbMsgSource; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -53,7 +54,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -64,28 +64,13 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @ExtendWith(MockitoExtension.class) public class TbGetTenantAttributeNodeTest { private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final TestDbCallbackExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock @@ -157,7 +142,7 @@ public class TbGetTenantAttributeNodeTest { assertThat(node.config).isEqualTo(config); assertThat(config.getDataMapping()).isEqualTo(Map.of("alarmThreshold", "threshold")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test @@ -168,7 +153,7 @@ public class TbGetTenantAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); config.setDataToFetch(DataToFetch.LATEST_TELEMETRY); - config.setFetchTo(FetchTo.DATA); + config.setFetchTo(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -181,7 +166,7 @@ public class TbGetTenantAttributeNodeTest { "sourceAttr2", "targetKey2", "sourceAttr3", "targetKey3")); assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY); - assertThat(node.fetchTo).isEqualTo(FetchTo.DATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.DATA); } @Test @@ -203,8 +188,8 @@ public class TbGetTenantAttributeNodeTest { @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -219,7 +204,7 @@ public class TbGetTenantAttributeNodeTest { // GIVEN var deviceId = new DeviceId(UUID.randomUUID()); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, deviceId); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.ATTRIBUTES, deviceId); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -231,7 +216,7 @@ public class TbGetTenantAttributeNodeTest { when(ctxMock.getTenantId()).thenReturn(TENANT_ID); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -260,7 +245,7 @@ public class TbGetTenantAttributeNodeTest { @Test public void givenFetchAttributesToMetaData_whenOnMsg_thenShouldFetchAttributesToMetaData() { // GIVEN - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, TENANT_ID); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.ATTRIBUTES, TENANT_ID); List attributesList = List.of( new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L), @@ -272,7 +257,7 @@ public class TbGetTenantAttributeNodeTest { when(ctxMock.getTenantId()).thenReturn(TENANT_ID); when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock); - when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) + when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(DataConstants.SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList)))) .thenReturn(Futures.immediateFuture(attributesList)); when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); @@ -303,7 +288,7 @@ public class TbGetTenantAttributeNodeTest { // GIVEN var customerId = new CustomerId(UUID.randomUUID()); - prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, customerId); + prepareMsgAndConfig(TbMsgSource.DATA, DataToFetch.LATEST_TELEMETRY, customerId); List timeseries = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -346,7 +331,7 @@ public class TbGetTenantAttributeNodeTest { // GIVEN var ruleChainId = new RuleChainId(UUID.randomUUID()); - prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, ruleChainId); + prepareMsgAndConfig(TbMsgSource.METADATA, DataToFetch.LATEST_TELEMETRY, ruleChainId); List timeseries = List.of( new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")), @@ -395,7 +380,7 @@ public class TbGetTenantAttributeNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, DataToFetch dataToFetch, EntityId originator) { config.setDataMapping(Map.of( "sourceKey1", "targetKey1", "${metaDataPattern1}", "$[messageBodyPattern1]", @@ -412,7 +397,7 @@ public class TbGetTenantAttributeNodeTest { var msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, msgMetaData, msgData); } @RequiredArgsConstructor diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java index 6e1e619eab..bfbaa9a92e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java @@ -29,9 +29,11 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.ContactBasedEntityDetails; +import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -102,14 +104,14 @@ public class TbGetTenantDetailsNodeTest { public void givenDefaultConfig_whenInit_thenOK() { // THEN assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList()); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.DATA); } @Test public void givenCustomConfig_whenInit_thenOK() throws TbNodeException { // GIVEN config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - config.setFetchTo(FetchTo.METADATA); + config.setFetchTo(TbMsgSource.METADATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); // WHEN @@ -118,15 +120,15 @@ public class TbGetTenantDetailsNodeTest { // THEN assertThat(node.config).isEqualTo(config); assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE)); - assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA); - assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA); + assertThat(config.getFetchTo()).isEqualTo(TbMsgSource.METADATA); + assertThat(node.fetchTo).isEqualTo(TbMsgSource.METADATA); } @Test public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() { // GIVEN - node.fetchTo = FetchTo.DATA; - msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]"); + node.fetchTo = TbMsgSource.DATA; + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_ARRAY); // WHEN var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg)); @@ -139,7 +141,7 @@ public class TbGetTenantDetailsNodeTest { @Test public void givenAllEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchAllToData() { // GIVEN - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values())); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.values())); mockFindTenant(); @@ -172,7 +174,7 @@ public class TbGetTenantDetailsNodeTest { @Test public void givenSomeEntityDetailsAndFetchToMetadata_whenOnMsg_thenShouldTellSuccessAndFetchSomeToMetaData() { // GIVEN - prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE)); + prepareMsgAndConfig(TbMsgSource.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE)); mockFindTenant(); @@ -201,7 +203,7 @@ public class TbGetTenantDetailsNodeTest { tenant.setAddress(null); tenant.setAddress2(null); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); mockFindTenant(); @@ -221,7 +223,7 @@ public class TbGetTenantDetailsNodeTest { @Test public void givenDidNotFindTenant_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() { // GIVEN - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2)); when(ctxMock.getTenantId()).thenReturn(tenant.getId()); when(ctxMock.getTenantService()).thenReturn(tenantServiceMock); @@ -245,7 +247,7 @@ public class TbGetTenantDetailsNodeTest { // GIVEN tenant.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":null}")); - prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO)); + prepareMsgAndConfig(TbMsgSource.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO)); mockFindTenant(); @@ -273,7 +275,7 @@ public class TbGetTenantDetailsNodeTest { Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass())); } - private void prepareMsgAndConfig(FetchTo fetchTo, List detailsList) { + private void prepareMsgAndConfig(TbMsgSource fetchTo, List detailsList) { config.setDetailsList(detailsList); config.setFetchTo(fetchTo); @@ -286,7 +288,7 @@ public class TbGetTenantDetailsNodeTest { var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}"; - msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); + msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData); } private void mockFindTenant() { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java index d33b2af030..eef77a5304 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java @@ -22,7 +22,6 @@ import org.mockito.ArgumentCaptor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @@ -39,12 +38,12 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; @@ -92,9 +91,10 @@ public class DeviceStateTest { }); when(ctx.getAlarmService()).thenReturn(alarmService); - when(ctx.newMsg(any(), any(), any(), any(), any(), any())).thenAnswer(invocationOnMock -> { + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), any())).thenAnswer(invocationOnMock -> { + TbMsgType type = invocationOnMock.getArgument(1); String data = invocationOnMock.getArgument(invocationOnMock.getArguments().length - 1); - return TbMsg.newMsg(null, null, new TbMsgMetaData(), data); + return TbMsg.newMsg(type, null, TbMsgMetaData.EMPTY, data); }); } @@ -106,8 +106,8 @@ public class DeviceStateTest { DeviceId deviceId = new DeviceId(UUID.randomUUID()); DeviceState deviceState = createDeviceState(deviceId, alarmConfig); - TbMsg attributeUpdateMsg = TbMsg.newMsg(SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), - deviceId, new TbMsgMetaData(), "{ \"enabled\": false }"); + TbMsg attributeUpdateMsg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, + deviceId, TbMsgMetaData.EMPTY, "{ \"enabled\": false }"); deviceState.process(ctx, attributeUpdateMsg); @@ -115,11 +115,11 @@ public class DeviceStateTest { verify(ctx).enqueueForTellNext(resultMsgCaptor.capture(), eq("Alarm Created")); Alarm alarm = JacksonUtil.fromString(resultMsgCaptor.getValue().getData(), Alarm.class); - deviceState.process(ctx, TbMsg.newMsg(DataConstants.ALARM_CLEAR, deviceId, new TbMsgMetaData(), JacksonUtil.toString(alarm))); + deviceState.process(ctx, TbMsg.newMsg(TbMsgType.ALARM_CLEAR, deviceId, TbMsgMetaData.EMPTY, JacksonUtil.toString(alarm))); reset(ctx); String deletedAttributes = "{ \"attributes\": [ \"other\" ] }"; - deviceState.process(ctx, TbMsg.newMsg(DataConstants.ATTRIBUTES_DELETED, deviceId, new TbMsgMetaData(), deletedAttributes)); + deviceState.process(ctx, TbMsg.newMsg(TbMsgType.ATTRIBUTES_DELETED, deviceId, TbMsgMetaData.EMPTY, deletedAttributes)); verify(ctx, never()).enqueueForTellNext(any(), anyString()); } @@ -129,17 +129,17 @@ public class DeviceStateTest { DeviceId deviceId = new DeviceId(UUID.randomUUID()); DeviceState deviceState = createDeviceState(deviceId, alarmConfig); - TbMsg attributeUpdateMsg = TbMsg.newMsg(SessionMsgType.POST_ATTRIBUTES_REQUEST.name(), - deviceId, new TbMsgMetaData(), "{ \"enabled\": false }"); + TbMsg attributeUpdateMsg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, + deviceId, TbMsgMetaData.EMPTY, "{ \"enabled\": false }"); deviceState.process(ctx, attributeUpdateMsg); ArgumentCaptor resultMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx).enqueueForTellNext(resultMsgCaptor.capture(), eq("Alarm Created")); Alarm alarm = JacksonUtil.fromString(resultMsgCaptor.getValue().getData(), Alarm.class); - deviceState.process(ctx, TbMsg.newMsg(DataConstants.ALARM_CLEAR, deviceId, new TbMsgMetaData(), JacksonUtil.toString(alarm))); + deviceState.process(ctx, TbMsg.newMsg(TbMsgType.ALARM_CLEAR, deviceId, TbMsgMetaData.EMPTY, JacksonUtil.toString(alarm))); - TbMsg alarmDeleteNotification = TbMsg.newMsg(DataConstants.ALARM_DELETE, deviceId, new TbMsgMetaData(), JacksonUtil.toString(alarm)); + TbMsg alarmDeleteNotification = TbMsg.newMsg(TbMsgType.ALARM_DELETE, deviceId, TbMsgMetaData.EMPTY, JacksonUtil.toString(alarm)); assertDoesNotThrow(() -> { deviceState.process(ctx, alarmDeleteNotification); }); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 80ed769a71..03a9c17a60 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -54,6 +54,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -64,7 +65,6 @@ import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; @@ -122,8 +122,8 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); - TbMsg msg = TbMsg.newMsg("123456789", deviceId, new TbMsgMetaData(), - TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); + TbMsg msg = TbMsg.newMsg("123456789", deviceId, TbMsgMetaData.EMPTY, + TbMsgDataType.JSON, JacksonUtil.toString(data)); node.onMsg(ctx, msg); verify(ctx).tellSuccess(msg); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); @@ -141,7 +141,7 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); verify(ctx).tellSuccess(msg); @@ -193,25 +193,25 @@ public class TbDeviceProfileNodeTest { Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); registerCreateAlarmMock(alarmService.createAlarm(any()), true); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())).thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 42); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); verify(ctx).tellSuccess(msg); verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); - TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + TbMsg theMsg2 = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, "2"); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())).thenReturn(theMsg2); registerCreateAlarmMock(alarmService.updateAlarm(any()), false); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); verify(ctx).tellSuccess(msg2); @@ -286,13 +286,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(attrListListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + Mockito.when(ctx.newMsg(Mockito.any(), Mockito.any(TbMsgType.class), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 21); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -373,13 +373,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) .thenReturn(attrListListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 21); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -442,13 +442,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -536,13 +536,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -557,7 +557,7 @@ public class TbDeviceProfileNodeTest { Thread.sleep(halfOfAlarmDelay); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -660,13 +660,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listNoDurationAttribute); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -681,7 +681,7 @@ public class TbDeviceProfileNodeTest { Thread.sleep(halfOfAlarmDelay); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -769,13 +769,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -784,7 +784,7 @@ public class TbDeviceProfileNodeTest { verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created"); data.put("temperature", 151); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -885,13 +885,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listNoDurationAttribute); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -900,7 +900,7 @@ public class TbDeviceProfileNodeTest { verify(ctx, Mockito.never()).tellNext(theMsg, "Alarm Created"); data.put("temperature", 151); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -981,13 +981,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1002,7 +1002,7 @@ public class TbDeviceProfileNodeTest { Thread.sleep(halfOfAlarmDelay); - TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg2 = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg2); @@ -1079,13 +1079,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1161,13 +1161,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureActiveSchedule); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); // Mockito.reset(ctx); @@ -1257,11 +1257,11 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureInactiveSchedule); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 35); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1335,13 +1335,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 25); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1408,13 +1408,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 40); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1491,13 +1491,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150L); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); @@ -1576,13 +1576,13 @@ public class TbDeviceProfileNodeTest { Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); - TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); - Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) + TbMsg theMsg = TbMsg.newMsg(TbMsgType.ALARM, deviceId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + when(ctx.newMsg(any(), any(TbMsgType.class), any(), any(), any(), Mockito.anyString())) .thenReturn(theMsg); ObjectNode data = JacksonUtil.newObjectNode(); data.put("temperature", 150L); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, JacksonUtil.toString(data), null, null); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java index 7b6a54ae0b..48aca3b573 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -32,6 +32,7 @@ import org.springframework.web.client.AsyncRestTemplate; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -144,16 +145,15 @@ public class TbHttpClientTest { var httpClient = new TbHttpClient(config, eventLoop); httpClient.setHttpClient(asyncRestTemplate); - var msg = TbMsg.newMsg("GET", new DeviceId(EntityId.NULL_UUID), TbMsgMetaData.EMPTY, "{}"); + var msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, new DeviceId(EntityId.NULL_UUID), TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); var successMsg = TbMsg.newMsg( - "SUCCESS", msg.getOriginator(), + TbMsgType.POST_TELEMETRY_REQUEST, msg.getOriginator(), msg.getMetaData(), msg.getData() ); var ctx = mock(TbContext.class); when(ctx.transformMsg( - eq(msg), eq(msg.getType()), - eq(msg.getOriginator()), + eq(msg), eq(msg.getMetaData()), eq(msg.getData()) )).thenReturn(successMsg); @@ -161,15 +161,14 @@ public class TbHttpClientTest { var capturedData = ArgumentCaptor.forClass(String.class); when(ctx.transformMsg( - eq(msg), eq(msg.getType()), - eq(msg.getOriginator()), + eq(msg), any(), capturedData.capture() )).thenReturn(successMsg); httpClient.processMessage(ctx, msg, m -> ctx.tellSuccess(msg), - (m, t) -> ctx.tellFailure(m, t)); + ctx::tellFailure); Awaitility.await() .atMost(30, TimeUnit.SECONDS) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java index 9185a31118..705f298513 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.rest; import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -39,6 +38,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -136,23 +136,18 @@ public class TbRestApiCallNodeTest { config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); initWithConfig(config); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); restNode.onMsg(ctx, msg); assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS)); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - - assertEquals("USER", typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); assertNotSame(metaData, metadataCaptor.getValue()); - assertEquals("{}", dataCaptor.getValue()); + assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); } @Test @@ -202,22 +197,18 @@ public class TbRestApiCallNodeTest { config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); initWithConfig(config); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); restNode.onMsg(ctx, msg); assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS)); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsg(msgCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); - assertEquals("USER", typeCaptor.getValue()); - assertEquals(originator, originatorCaptor.getValue()); assertNotSame(metaData, metadataCaptor.getValue()); - assertEquals("{}", dataCaptor.getValue()); + assertEquals(TbMsg.EMPTY_JSON_OBJECT, dataCaptor.getValue()); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java index 27faa8313a..7b12dbb2b1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNodeTest.java @@ -31,10 +31,10 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.dao.edge.EdgeEventService; import java.util.UUID; @@ -80,7 +80,7 @@ public class TbSendRPCReplyNodeTest { Mockito.when(ctx.getRpcService()).thenReturn(rpcService); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, getDefaultMetadata(), + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, getDefaultMetadata(), TbMsgDataType.JSON, DUMMY_DATA, null, null); node.onMsg(ctx, msg); @@ -99,7 +99,7 @@ public class TbSendRPCReplyNodeTest { TbMsgMetaData defaultMetadata = getDefaultMetadata(); defaultMetadata.putValue(DataConstants.EDGE_ID, UUID.randomUUID().toString()); defaultMetadata.putValue(DataConstants.DEVICE_ID, UUID.randomUUID().toString()); - TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, defaultMetadata, + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, deviceId, defaultMetadata, TbMsgDataType.JSON, DUMMY_DATA, null, null); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java index c0839f5b61..632243676f 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNodeTest.java @@ -25,7 +25,9 @@ import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -49,10 +51,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; -import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; -import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE; @Slf4j public class TbMsgDeleteAttributesNodeTest { @@ -94,7 +92,7 @@ public class TbMsgDeleteAttributesNodeTest { @Test void givenDefaultConfig_whenVerify_thenOK() { TbMsgDeleteAttributesNodeConfiguration defaultConfig = new TbMsgDeleteAttributesNodeConfiguration().defaultConfiguration(); - assertThat(defaultConfig.getScope()).isEqualTo(SERVER_SCOPE); + assertThat(defaultConfig.getScope()).isEqualTo(DataConstants.SERVER_SCOPE); assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptyList()); } @@ -115,7 +113,7 @@ public class TbMsgDeleteAttributesNodeTest { void givenMsg_whenOnMsg_thenVerifyOutput_SendAttributesDeletedNotification_NotifyDevice() throws Exception { config.setSendAttributesDeletedNotification(true); config.setNotifyDevice(true); - config.setScope(SHARED_SCOPE); + config.setScope(DataConstants.SHARED_SCOPE); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); onMsg_thenVerifyOutput(true, true, false); @@ -135,12 +133,12 @@ public class TbMsgDeleteAttributesNodeTest { ); TbMsgMetaData metaData = new TbMsgMetaData(mdMap); if (notifyDeviceMetadata) { - metaData.putValue(NOTIFY_DEVICE_METADATA_KEY, "true"); - metaData.putValue(SCOPE, SHARED_SCOPE); + metaData.putValue(DataConstants.NOTIFY_DEVICE_METADATA_KEY, "true"); + metaData.putValue(DataConstants.SCOPE, DataConstants.SHARED_SCOPE); } final String data = "{\"TestAttribute_2\": \"humidity\", \"TestAttribute_3\": \"voltage\"}"; - TbMsg msg = TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", deviceId, metaData, data, callback); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, deviceId, metaData, data, callback); node.onMsg(ctx, msg); ArgumentCaptor successCaptor = ArgumentCaptor.forClass(Runnable.class); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java index f4cd097878..8ea7806ae2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.transform; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,6 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -35,13 +35,13 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.asset.AssetService; import java.util.NoSuchElementException; -import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -66,21 +66,7 @@ public class TbChangeOriginatorNodeTest { @Before public void before() { - dbExecutor = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(Runnable command) { - command.run(); - } - }; + dbExecutor = new TestDbCallbackExecutor(); } @Test @@ -94,7 +80,7 @@ public class TbChangeOriginatorNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON, TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(ctx.getAssetService()).thenReturn(assetService); when(assetService.findAssetByIdAsync(any(),eq( assetId))).thenReturn(Futures.immediateFuture(asset)); @@ -102,11 +88,8 @@ public class TbChangeOriginatorNodeTest { node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); assertEquals(customerId, originatorCaptor.getValue()); } @@ -122,18 +105,15 @@ public class TbChangeOriginatorNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON,"{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON,TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(ctx.getAssetService()).thenReturn(assetService); when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset)); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); - ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); - ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); - ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + verify(ctx).transformMsgOriginator(msgCaptor.capture(), originatorCaptor.capture()); assertEquals(customerId, originatorCaptor.getValue()); } @@ -149,7 +129,7 @@ public class TbChangeOriginatorNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "ASSET", assetId, new TbMsgMetaData(), TbMsgDataType.JSON,"{}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, assetId, TbMsgMetaData.EMPTY, TbMsgDataType.JSON,TbMsg.EMPTY_JSON_OBJECT, ruleChainId, ruleNodeId); when(ctx.getAssetService()).thenReturn(assetService); when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(null)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java index 2666cae477..e0ab34e35b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java @@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -78,8 +79,7 @@ public class TbCopyKeysNodeTest { @Test void givenMsgFromMetadata_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "{}"; - node.onMsg(ctx, getTbMsg(deviceId, data)); + node.onMsg(ctx, getTbMsg(deviceId, TbMsg.EMPTY_JSON_OBJECT)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); @@ -136,8 +136,7 @@ public class TbCopyKeysNodeTest { @Test void givenMsgDataNotJSONObject_whenOnMsg_thenTVerifyOutput() throws Exception { - String data = "[]"; - TbMsg msg = getTbMsg(deviceId, data); + TbMsg msg = getTbMsg(deviceId, TbMsg.EMPTY_JSON_ARRAY); node.onMsg(ctx, msg); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -157,7 +156,7 @@ public class TbCopyKeysNodeTest { "voltageDataValue", "220", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java index 7152c25acd..6eee16e7d3 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java @@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -78,8 +79,7 @@ public class TbDeleteKeysNodeTest { @Test void givenMsgFromMetadata_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "{}"; - node.onMsg(ctx, getTbMsg(deviceId, data)); + node.onMsg(ctx, getTbMsg(deviceId, TbMsg.EMPTY_JSON_OBJECT)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); @@ -140,7 +140,7 @@ public class TbDeleteKeysNodeTest { "voltageDataValue", "220", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java index 0ca486a3e0..a86d902007 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbJsonPathNodeTest.java @@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -170,6 +171,6 @@ public class TbJsonPathNodeTest { Map mdMap = Map.of("country", "US", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java index 7bfa5c8bfc..be6aea6fa5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbMsgDeduplicationNodeTest.java @@ -30,7 +30,6 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.deduplication.DeduplicationStrategy; import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode; import org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNodeConfiguration; @@ -39,9 +38,10 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.common.msg.session.SessionMsgType; import java.util.ArrayList; import java.util.List; @@ -70,8 +70,6 @@ import static org.mockito.Mockito.when; @Slf4j public class TbMsgDeduplicationNodeTest { - private static final String TB_MSG_DEDUPLICATION_TIMEOUT_MSG = "TbMsgDeduplicationNodeMsg"; - private TbContext ctx; private final ThingsBoardThreadFactory factory = ThingsBoardThreadFactory.forName("de-duplication-node-test"); @@ -97,12 +95,12 @@ public class TbMsgDeduplicationNodeTest { when(ctx.getTenantId()).thenReturn(tenantId); doAnswer((Answer) invocationOnMock -> { - String type = (String) (invocationOnMock.getArguments())[1]; + TbMsgType type = (TbMsgType) (invocationOnMock.getArguments())[1]; EntityId originator = (EntityId) (invocationOnMock.getArguments())[2]; TbMsgMetaData metaData = (TbMsgMetaData) (invocationOnMock.getArguments())[3]; String data = (String) (invocationOnMock.getArguments())[4]; return TbMsg.newMsg(type, originator, metaData.copy(), data); - }).when(ctx).newMsg(isNull(), eq(TB_MSG_DEDUPLICATION_TIMEOUT_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); + }).when(ctx).newMsg(isNull(), eq(TbMsgType.DEDUPLICATION_TIMEOUT_SELF_MSG), nullable(EntityId.class), any(TbMsgMetaData.class), any(String.class)); node = spy(new TbMsgDeduplicationNode()); config = new TbMsgDeduplicationNodeConfiguration().defaultConfiguration(); } @@ -173,7 +171,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); TbMsg firstMsg = inputMsgs.get(0); TbMsg actualMsg = newMsgCaptor.getValue(); @@ -221,7 +219,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(ctx, times(1)).tellFailure(eq(msgToReject), any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation + 1)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); TbMsg actualMsg = newMsgCaptor.getValue(); // msg ids should be different because we create new msg before enqueueForTellNext @@ -242,7 +240,7 @@ public class TbMsgDeduplicationNodeTest { config.setInterval(deduplicationInterval); config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); + config.setOutMsgType(TbMsgType.POST_ATTRIBUTES_REQUEST.name()); config.setQueueName(DataConstants.HP_QUEUE_NAME); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); @@ -263,7 +261,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(1)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); Assertions.assertEquals(1, newMsgCaptor.getAllValues().size()); TbMsg outMessage = newMsgCaptor.getAllValues().get(0); @@ -282,7 +280,7 @@ public class TbMsgDeduplicationNodeTest { config.setInterval(deduplicationInterval); config.setStrategy(DeduplicationStrategy.ALL); - config.setOutMsgType(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()); + config.setOutMsgType(TbMsgType.POST_ATTRIBUTES_REQUEST.name()); config.setQueueName(DataConstants.HP_QUEUE_NAME); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); @@ -309,7 +307,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); List resultMsgs = newMsgCaptor.getAllValues(); Assertions.assertEquals(2, resultMsgs.size()); @@ -363,7 +361,7 @@ public class TbMsgDeduplicationNodeTest { verify(ctx, times(msgCount)).ack(any()); verify(node, times(msgCount + wantedNumberOfTellSelfInvocation)).onMsg(eq(ctx), any()); - verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbRelationTypes.SUCCESS), successCaptor.capture(), failureCaptor.capture()); + verify(ctx, times(2)).enqueueForTellNext(newMsgCaptor.capture(), eq(TbNodeConnectionType.SUCCESS), successCaptor.capture(), failureCaptor.capture()); List resultMsgs = newMsgCaptor.getAllValues(); Assertions.assertEquals(2, resultMsgs.size()); @@ -414,7 +412,7 @@ public class TbMsgDeduplicationNodeTest { metaData.putValue("ts", String.valueOf(ts)); return TbMsg.newMsg( DataConstants.MAIN_QUEUE_NAME, - SessionMsgType.POST_TELEMETRY_REQUEST.name(), + TbMsgType.POST_TELEMETRY_REQUEST, deviceId, metaData, JacksonUtil.toString(dataNode)); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java index 9eade69e4b..c602aab5df 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java @@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -134,8 +135,7 @@ public class TbRenameKeysNodeTest { @Test void givenMsgDataNotJSONObject_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "[]"; - TbMsg msg = getTbMsg(deviceId, data); + TbMsg msg = getTbMsg(deviceId, TbMsg.EMPTY_JSON_ARRAY); node.onMsg(ctx, msg); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -154,6 +154,6 @@ public class TbRenameKeysNodeTest { "country", "US", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java index 67ebf6f6fc..e7e2ae16b2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNodeTest.java @@ -27,6 +27,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -82,8 +83,7 @@ public class TbSplitArrayMsgNodeTest { @Test void givenZeroMsg_whenOnMsg_thenVerifyOutput() throws Exception { - String data = "[]"; - VerifyOutputMsg(data); + VerifyOutputMsg(TbMsg.EMPTY_JSON_ARRAY); } @Test @@ -132,6 +132,6 @@ public class TbSplitArrayMsgNodeTest { "country", "US", "city", "NY" ); - return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, new TbMsgMetaData(mdMap), data, callback); + return TbMsg.newMsg(TbMsgType.POST_ATTRIBUTES_REQUEST, entityId, new TbMsgMetaData(mdMap), data, callback); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index 71348a1a23..56c94fd4ff 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -29,6 +29,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -60,8 +61,8 @@ public class TbTransformMsgNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); - TbMsg transformedMsg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); + TbMsg transformedMsg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(Collections.singletonList(transformedMsg))); node.onMsg(ctx, msg); @@ -80,7 +81,7 @@ public class TbTransformMsgNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); - TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("error"))); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java index 957f98d721..c675b97d08 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java @@ -16,13 +16,12 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.Customer; @@ -41,7 +40,6 @@ import org.thingsboard.server.dao.user.UserService; import java.util.EnumSet; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -60,21 +58,7 @@ public class EntitiesCustomerIdAsyncLoaderTest { EntityType.ASSET, EntityType.DEVICE ); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java index e835fa6e78..d60f2256e1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoaderTest.java @@ -16,8 +16,6 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; @@ -61,7 +60,6 @@ import org.thingsboard.server.dao.user.UserService; import java.util.EnumSet; import java.util.NoSuchElementException; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static org.assertj.core.api.Assertions.assertThat; @@ -75,21 +73,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class EntitiesFieldsAsyncLoaderTest { - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); private static EnumSet SUPPORTED_ENTITY_TYPES; private static UUID RANDOM_UUID; private static TenantId TENANT_ID; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java index 546a726f07..c54072fb53 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoaderTest.java @@ -16,13 +16,12 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.data.DeviceRelationsQuery; import org.thingsboard.server.common.data.Device; @@ -37,7 +36,6 @@ import org.thingsboard.server.dao.device.DeviceService; import java.util.List; import java.util.UUID; -import java.util.concurrent.Callable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -51,21 +49,7 @@ public class EntitiesRelatedDeviceIdAsyncLoaderTest { private static final EntityId DUMMY_ORIGINATOR = new DeviceId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); @Mock private TbContext ctxMock; @Mock diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java index b29e451eaf..e51a3704d1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoaderTest.java @@ -17,11 +17,11 @@ package org.thingsboard.rule.engine.util; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.TestDbCallbackExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.Device; @@ -41,7 +41,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.concurrent.Callable; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -57,21 +56,7 @@ public class EntitiesRelatedEntityIdAsyncLoaderTest { private static final EntityId ASSET_ORIGINATOR_ID = new AssetId(UUID.randomUUID()); private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID()); - private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() { - @Override - public ListenableFuture executeAsync(Callable task) { - try { - return Futures.immediateFuture(task.call()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } - }; + private static final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); private TbContext ctxMock; private RelationService relationServiceMock; diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index b9db930657..1f8861ced9 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" cache: type: "${CACHE_TYPE:redis}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index bff7adb561..7c5103cfac 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -68,6 +68,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" cache: type: "${CACHE_TYPE:redis}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index ae8f0138a7..4ab59aec01 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" cache: type: "${CACHE_TYPE:redis}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 076dde0234..a103edf1f4 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" cache: type: "${CACHE_TYPE:redis}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index c68c9c56a8..44a86dc6dd 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -41,6 +41,7 @@ zk: session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" # Name of the directory in zookeeper 'filesystem' zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:60000}" cache: type: "${CACHE_TYPE:redis}" diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 6ad217e20f..67e2fd7b42 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -30,7 +30,7 @@ import { combineLatest } from 'rxjs'; import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators'; import { AuthService } from '@core/auth/auth.service'; -import { svgIcons } from '@shared/models/icon.models'; +import { svgIcons, svgIconsUrl } from '@shared/models/icon.models'; @Component({ selector: 'tb-root', @@ -65,6 +65,10 @@ export class AppComponent implements OnInit { ); } + for (const svgIcon of Object.keys(svgIconsUrl)) { + this.matIconRegistry.addSvgIcon(svgIcon, this.domSanitizer.bypassSecurityTrustResourceUrl(svgIconsUrl[svgIcon])); + } + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/core/auth/auth.actions.ts b/ui-ngx/src/app/core/auth/auth.actions.ts index 67b122d67f..9e5640a97d 100644 --- a/ui-ngx/src/app/core/auth/auth.actions.ts +++ b/ui-ngx/src/app/core/auth/auth.actions.ts @@ -17,6 +17,7 @@ import { Action } from '@ngrx/store'; import { User } from '@shared/models/user.model'; import { AuthPayload } from '@core/auth/auth.models'; +import { UserSettings } from '@shared/models/user-settings.models'; export enum AuthActionTypes { AUTHENTICATED = '[Auth] Authenticated', @@ -25,7 +26,9 @@ export enum AuthActionTypes { UPDATE_USER_DETAILS = '[Auth] Update User Details', UPDATE_LAST_PUBLIC_DASHBOARD_ID = '[Auth] Update Last Public Dashboard Id', UPDATE_HAS_REPOSITORY = '[Auth] Change Has Repository', - UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section' + UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section', + PUT_USER_SETTINGS = '[Preferences] Put user settings', + DELETE_USER_SETTINGS = '[Preferences] Delete user settings', } export class ActionAuthAuthenticated implements Action { @@ -68,6 +71,18 @@ export class ActionPreferencesUpdateOpenedMenuSection implements Action { constructor(readonly payload: { path: string; opened: boolean }) {} } +export class ActionPreferencesPutUserSettings implements Action { + readonly type = AuthActionTypes.PUT_USER_SETTINGS; + + constructor(readonly payload: Partial) {} +} + +export class ActionPreferencesDeleteUserSettings implements Action { + readonly type = AuthActionTypes.DELETE_USER_SETTINGS; + + constructor(readonly payload: Array>) {} +} + export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository | - ActionPreferencesUpdateOpenedMenuSection; + ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings; diff --git a/ui-ngx/src/app/core/auth/auth.effects.ts b/ui-ngx/src/app/core/auth/auth.effects.ts index d2ebe2b4c5..3e5eb28d72 100644 --- a/ui-ngx/src/app/core/auth/auth.effects.ts +++ b/ui-ngx/src/app/core/auth/auth.effects.ts @@ -39,4 +39,18 @@ export class AuthEffects { withLatestFrom(this.store.pipe(select(selectAuthState))), mergeMap(([action, state]) => this.userSettingsService.putUserSettings({ openedMenuSections: state.userSettings.openedMenuSections })) ), {dispatch: false}); + + putUserSettings = createEffect(() => this.actions$.pipe( + ofType( + AuthActionTypes.PUT_USER_SETTINGS, + ), + mergeMap((state) => this.userSettingsService.putUserSettings(state.payload)) + ), {dispatch: false}); + + deleteUserSettings = createEffect(() => this.actions$.pipe( + ofType( + AuthActionTypes.DELETE_USER_SETTINGS, + ), + mergeMap((state) => this.userSettingsService.deleteUserSettings(state.payload)) + ), {dispatch: false}); } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 0847654399..4bcf71104b 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -16,7 +16,8 @@ import { AuthPayload, AuthState } from './auth.models'; import { AuthActions, AuthActionTypes } from './auth.actions'; -import { initialUserSettings } from '@shared/models/user-settings.models'; +import { initialUserSettings, UserSettings } from '@shared/models/user-settings.models'; +import { unset } from '@core/utils'; const emptyUserAuthState: AuthPayload = { authUser: null, @@ -42,6 +43,7 @@ export const authReducer = ( state: AuthState = initialState, action: AuthActions ): AuthState => { + let userSettings: UserSettings; switch (action.type) { case AuthActionTypes.AUTHENTICATED: return { ...state, isAuthenticated: true, ...action.payload }; @@ -71,7 +73,16 @@ export const authReducer = ( } else { openedMenuSections.delete(action.payload.path); } - const userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}}; + userSettings = {...state.userSettings, ...{ openedMenuSections: Array.from(openedMenuSections)}}; + return { ...state, ...{ userSettings }}; + + case AuthActionTypes.PUT_USER_SETTINGS: + userSettings = {...state.userSettings, ...action.payload}; + return { ...state, ...{ userSettings }}; + + case AuthActionTypes.DELETE_USER_SETTINGS: + userSettings = {...state.userSettings}; + action.payload.forEach(path => unset(userSettings, path)); return { ...state, ...{ userSettings }}; default: diff --git a/ui-ngx/src/app/core/auth/auth.selectors.ts b/ui-ngx/src/app/core/auth/auth.selectors.ts index 2e8406293e..4bf2f2276d 100644 --- a/ui-ngx/src/app/core/auth/auth.selectors.ts +++ b/ui-ngx/src/app/core/auth/auth.selectors.ts @@ -21,6 +21,7 @@ import { AuthState } from './auth.models'; import { take } from 'rxjs/operators'; import { AuthUser } from '@shared/models/user.model'; import { UserSettings } from '@shared/models/user-settings.models'; +import { getDescendantProp } from '@core/utils'; export const selectAuthState = createFeatureSelector< AuthState>( 'auth' @@ -76,6 +77,11 @@ export const selectUserSettings = createSelector( (state: AuthState) => state.userSettings ); +export const selectUserSettingsProperty = (path: NestedKeyOf) => createSelector( + selectAuthState, + (state: AuthState) => getDescendantProp(state.userSettings, path) +); + export const selectOpenedMenuSections = createSelector( selectAuthState, (state: AuthState) => state.userSettings.openedMenuSections diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 87c6561315..f1af3b745a 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -244,7 +244,7 @@ export class AuthService { if (authState && authState.authUser) { if (authState.authUser.authority === Authority.TENANT_ADMIN || authState.authUser.authority === Authority.CUSTOMER_USER) { if ((this.userHasDefaultDashboard(authState) && authState.forceFullscreen) || authState.authUser.isPublic) { - if (path === 'profile' || path === 'security') { + if (path.startsWith('account')) { if (this.userHasProfile(authState.authUser)) { return false; } else { diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index dfc2d674a2..8dff1e7ebc 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -25,8 +25,10 @@ import { ClaimResult, Device, DeviceCredentials, - DeviceInfo, DeviceInfoQuery, - DeviceSearchQuery + DeviceInfo, + DeviceInfoQuery, + DeviceSearchQuery, + PublishTelemetryCommand } from '@app/shared/models/device.models'; import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; @@ -208,4 +210,8 @@ export class DeviceService { return this.http.post('/api/device/bulk_import', entitiesData, defaultHttpOptionsFromConfig(config)); } + public getDevicePublishTelemetryCommands(deviceId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/device-connectivity/${deviceId}`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 8355ce2f61..a2a33b73a1 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -348,10 +348,8 @@ export class DashboardUtilsService { private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, datasources?: Datasource[]): Datasource[] { const newDatasources: Datasource[] = []; - if (datasources) { - datasources.forEach(datasource => { - newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource)); - }); + if (datasources?.length) { + newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasources[0])); } return newDatasources; } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 7d11e60b1f..d7af7425c9 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -322,6 +322,8 @@ export const isEqual = (a: any, b: any): boolean => _.isEqual(a, b); export const isEmpty = (a: any): boolean => _.isEmpty(a); +export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path); + export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { if (a === b) { return true; @@ -781,3 +783,25 @@ export function genNextLabel(name: string, datasources: Datasource[]): string { } return label; } + +export const getOS = (): string => { + const userAgent = window.navigator.userAgent.toLowerCase(); + const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos|mac_powerpc)/i; + const windowsPlatforms = /(win32|win64|windows|wince)/i; + const iosPlatforms = /(iphone|ipad|ipod|darwin|ios)/i; + let os = null; + + if (macosPlatforms.test(userAgent)) { + os = 'macos'; + } else if (iosPlatforms.test(userAgent)) { + os = 'ios'; + } else if (windowsPlatforms.test(userAgent)) { + os = 'windows'; + } else if (/android/.test(userAgent)) { + os = 'android'; + } else if (/linux/.test(userAgent)) { + os = 'linux'; + } + + return os; +}; diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html index c5ed4fe52a..cbbcc2ccb9 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html @@ -56,25 +56,28 @@
-
-
alarm.alarm-status-list
+
+
alarm.alarm-status-list
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
-
-
alarm.alarm-severity-list
+
+
alarm.alarm-severity-list
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
-
-
alarm.alarm-type-list
- +
+
alarm.alarm-type-list
+ @@ -89,9 +92,10 @@
-
-
alarm.assignee
+
+
alarm.assignee
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss index 1c10e244b5..95f78f8fde 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss @@ -15,11 +15,24 @@ */ :host { display: block; - overflow: hidden; + overflow: scroll; max-width: 100%; .mdc-button { max-width: 100%; } + + .filters-row-mobile { + flex-direction: column; + align-items: start; + border: none; + padding: 0; + } + .filters-title-mobile { + font-size: 14px; + } + .filters-fields-width-mobile { + width: 100%; + } } :host ::ng-deep { @@ -32,4 +45,9 @@ text-overflow: ellipsis; } } + .mat-mdc-chip { + .mdc-evolution-chip__cell, .mat-mdc-chip-action, .mat-mdc-chip-action-label { + overflow: hidden; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss index 831762d1e4..b33dfdbb20 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss @@ -104,7 +104,7 @@ } mat-cell.tb-value-cell { cursor: pointer; - mat-icon { + .mat-icon { height: 24px; width: 24px; font-size: 24px; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html index 7a17157b31..7b6939df52 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html @@ -17,14 +17,17 @@ -->
-

widget.add

- : {{data.widgetInfo.widgetName}} -
- - {{ 'widget.basic-mode' | translate }} - {{ 'widget.advanced-mode' | translate }} - -
+
+

{{'widget.add' | translate}}:

+ {{data.widgetInfo.widgetName}} +
+ + {{ 'widget.basic-mode' | translate }} + {{ 'widget.advanced-mode' | translate }} + +
+
-
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 6432a3f234..d58c057b68 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -25,8 +25,7 @@ [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
-
+
@@ -74,8 +73,7 @@
-
+
+ + + + @@ -360,7 +371,7 @@ [isReadOnly]="true" (closeDetails)="onEditWidgetClosed()">
- + {{ 'widget.basic-mode' | translate }} {{ 'widget.advanced-mode' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 39940546c2..3996c1abaf 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -191,7 +191,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } get hideToolbar(): boolean { - return (this.hideToolbarValue || this.hideToolbarSetting()) && !this.isEdit; + return ((this.hideToolbarValue || this.hideToolbarSetting()) && !this.isEdit) || (this.isEditingWidget || this.isAddingWidget); } @Input() @@ -1177,6 +1177,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC Widget>(AddWidgetDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + maxWidth: '95vw', data: { dashboard: this.dashboard, aliasController: this.dashboardCtx.aliasController, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss index 0d9ade9bf6..e43132c761 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss @@ -126,6 +126,7 @@ tb-dashboard-toolbar { @media #{$mat-lt-md} { height: $mobile-toolbar-height; max-height: $mobile-toolbar-height; + padding: 0 8px !important; } .close-action { @@ -150,8 +151,44 @@ tb-dashboard-toolbar { .tb-dashboard-action-panel { min-width: 0; height: $half-mobile-toolbar-height; + flex: 1 0 auto; + display: flex; + flex-direction: row-reverse; + place-content: center space-between; + align-items: center; + &.tb-left-panel { + flex: 1 1 auto; + } + + @media #{$mat-lt-md} { + padding-left: 12px; + } + + @media #{$mat-xs} { + gap: 3px; + padding-left: 0; + &.tb-left-panel { + padding-left: 12px; + } + } + + @media #{$mat-sm} { + gap: 6px; + } + + @media #{$mat-md} { + gap: 6px; + } + + @media #{$mat-gt-md} { + gap: 12px; + } @media #{$mat-gt-sm} { + place-content: center flex-start; + &.tb-left-panel { + place-content: center flex-end; + } height: 46px; } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html index a1c8ee6ade..86f069239c 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html @@ -32,26 +32,38 @@ chevron_left {{ 'action.back' | translate }} -
-
- - -
+
+ +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss index c31f1d5791..9c4e20a7a5 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss @@ -16,7 +16,7 @@ :host { .widget-preview-background { position: absolute; - top: 72px; + top: 68px; left: 0; right: 0; bottom: 0; diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.html b/ui-ngx/src/app/modules/home/components/details-panel.component.html index 15df7e99e5..748723197f 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.html @@ -21,7 +21,7 @@
- {{ headerTitle }} + {{ headerTitle }}
{{ headerSubtitle }} diff --git a/ui-ngx/src/app/modules/home/components/details-panel.component.scss b/ui-ngx/src/app/modules/home/components/details-panel.component.scss index 9002246841..451795a2d1 100644 --- a/ui-ngx/src/app/modules/home/components/details-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/details-panel.component.scss @@ -32,16 +32,14 @@ max-height: 120px; &.tb-details-title-header { min-width: 0; + padding: 0 16px 0 8px; } } .tb-details-title { width: inherit; margin: 20px 8px 0 0; - overflow: hidden; font-size: 1rem; font-weight: 400; - text-overflow: ellipsis; - white-space: nowrap; @media #{$mat-gt-sm} { font-size: 1.5rem; @@ -49,13 +47,16 @@ } .tb-details-subtitle { - width: inherit; margin: 10px 0; - overflow: hidden; font-size: 1rem; + opacity: .8; + } + + .tb-details-title-text, .tb-details-subtitle { + width: inherit; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - opacity: .8; } tb-dashboard { diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 026624dbf3..d574a3593a 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -360,7 +360,7 @@ export class EventTableConfig extends EntityTableConfig { this.cellActionDescriptors.push({ name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}), icon: 'bug_report', - isEnabled: (entity) => entity.body.type === 'IN', + isEnabled: (entity) => entity.body.type === 'IN' || entity.body.error !== undefined, onAction: ($event, entity) => { this.debugEventSelected.next(entity.body); } diff --git a/ui-ngx/src/app/modules/home/components/router-tabs.component.html b/ui-ngx/src/app/modules/home/components/router-tabs.component.html index f16a761c3e..5ad09403c2 100644 --- a/ui-ngx/src/app/modules/home/components/router-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/router-tabs.component.html @@ -20,6 +20,7 @@
-
+
widget-config.show-card-buttons
{{ 'action.search' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss index d29594fce3..0f88b8f1dc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss @@ -13,8 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +@import '../../../../../../../scss/constants'; + :host { display: flex; flex-direction: column; gap: 16px; + @media #{$mat-xs} { + gap: 8px; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html index c5501ae830..c68caab86e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html @@ -61,7 +61,7 @@
-
+
widget-config.show-card-buttons
{{ 'action.search' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html index 142f32cd4e..92bd2e44ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html @@ -61,7 +61,7 @@
-
+
widget-config.show-card-buttons
{{ 'fullscreen.fullscreen' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html index bab6437485..49196fac3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/timeseries-table-basic-config.component.html @@ -61,7 +61,7 @@
-
+
widget-config.show-card-buttons
{{ 'action.search' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html index b5808a8c96..356612564f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html @@ -45,7 +45,7 @@ {{ 'widgets.value-card.label' | translate }}
- +
-
+
{{ 'widgets.value-card.icon' | translate }}
- + @@ -75,10 +75,10 @@
widgets.value-card.value
- - + + -
widget-config.decimals-suffix
+
widget-config.decimals-suffix
@@ -87,18 +87,38 @@
-
-
- - {{ 'widgets.value-card.date' | translate }} - -
- - - - - +
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + +
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts index 762b26ac42..f00ec8e2e6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts @@ -74,6 +74,16 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent { datePreviewFn = this._datePreviewFn.bind(this); + get dateEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value; + return ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + } + + get iconEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value; + return layout !== ValueCardLayout.simplified; + } + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private cd: ChangeDetectorRef, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html index 1439f74931..952b3f9031 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/flot-basic-config.component.html @@ -61,7 +61,7 @@
-
+
widget-config.show-card-buttons
{{ 'fullscreen.fullscreen' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html index b603f98a4d..63f936195a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html @@ -22,13 +22,13 @@ {{ 'datakey.latest' | translate }} - +
-
+
timeline
-
+
@@ -139,7 +139,7 @@ - +
@@ -158,7 +158,8 @@
-
+
+ datakey.data-generation-func +
+ + +
datakey.aggregation @@ -80,72 +92,62 @@ {{ 'datakey.aggregation-type-hint-common' | translate }}
-
- datakey.delta-calculation - - - - - {{ 'datakey.enable-delta-calculation' | translate }} - - {{ 'datakey.enable-delta-calculation-hint' | translate }} - - - -
- - widgets.chart.time-for-comparison - - - {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} - - - {{ 'widgets.chart.time-for-comparison-days' | translate }} - - - {{ 'widgets.chart.time-for-comparison-weeks' | translate }} - - - {{ 'widgets.chart.time-for-comparison-months' | translate }} - - - {{ 'widgets.chart.time-for-comparison-years' | translate }} - - - {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} - - - - - widgets.chart.custom-interval-value - - - - datakey.delta-calculation-result - - - {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} - - - -
-
-
-
-
- datakey.data-generation-func -
- - -
+
+
datakey.delta-calculation
+ + + + + {{ 'datakey.enable-delta-calculation' | translate }} + + {{ 'datakey.enable-delta-calculation-hint' | translate }} + + + +
+ + widgets.chart.time-for-comparison + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + widgets.chart.custom-interval-value + + + + datakey.delta-calculation-result + + + {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html index 0b4c774393..3226ffed5e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html @@ -47,7 +47,7 @@ drag_indicator
-
+
{{key.label}}
:
-
+
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss index 7d01f41cb5..574d1df38e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss @@ -49,7 +49,7 @@ padding: 3px; height: 24px; cursor: move; - mat-icon { + .mat-icon { pointer-events: none; } } @@ -65,9 +65,14 @@ font-weight: normal; font-size: 14px; line-height: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + &.tb-chip-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + &.tb-chip-icon { + min-width: 24px; + } .mat-icon.tb-datakey-icon { margin-right: 4px; margin-left: 4px; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html index 2e4a984858..0ee6a7c508 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html @@ -44,29 +44,27 @@ [cdkDropListDisabled]="dragDisabled" formArrayName="datasources"> -
-
+
+
{{$index + 1}}
-
-
- +
+
+ -
-
-
-
-
+
+
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
-
+
{{ 'widgets.input-widgets.not-allowed-entity' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss index d0dd324e52..3185bc8b17 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -21,7 +21,7 @@ flex-direction: column; .tb-multiple-input-container { - padding: 0 8px; + padding: 8px 8px 0; flex: 1 1 100%; overflow-x: hidden; overflow-y: auto; @@ -37,6 +37,28 @@ } } + .tb-multiple-input-layout { + display: flex; + flex-direction: row; + align-items: start; + } + + .color-picker-input { + padding: 7px 16px 7px 12px; + margin: 0 10px 22px 0; + border-color: rgba(0, 0, 0, 0.4); + + .label-container { + display: flex; + flex-direction: row; + align-items: center; + } + + .mat-icon, img { + margin-right: 5px; + } + } + .input-field { padding-right: 10px; @@ -61,6 +83,30 @@ .vertical-alignment { flex-direction: column; } + + &--buttons-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + &__button { + max-height: 50px; + margin-right:20px; + } + } + + &--errors-container { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + &__error { + text-align: center; + font-size: 18px; + color: #a0a0a0; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index 534be7c676..6f29a495cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -54,7 +54,7 @@ type FieldAlignment = 'row' | 'column'; type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; export type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | 'JSON' | 'booleanCheckbox' | 'booleanSwitch' | - 'dateTime' | 'date' | 'time' | 'select'; + 'dateTime' | 'date' | 'time' | 'select' | 'color'; type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any; @@ -390,6 +390,12 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } }); } + } else if (key.settings.dataKeyValueType === 'color') { + formControl.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(() => { + this.inputChanged(source, key); + }); } this.multipleInputFormGroup.addControl(key.formId, formControl); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html index dadf3317d8..77da2cabbb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html @@ -16,6 +16,6 @@ --> - {{settings.icon}} + {{settings.icon}} {{translatedName}} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss index a04f82dce7..b9c3e034a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss @@ -31,7 +31,7 @@ display: flex; flex-direction: column; align-items: center; - mat-icon { + .mat-icon { margin: auto !important; } span.mdc-button__label { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html index 9baf6c998f..f408c6d93e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html @@ -62,8 +62,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -86,8 +86,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index 4ec8f31be6..e304def2c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -94,7 +94,7 @@ {{ 'widgets.table.display-pagination' | translate }} -
+
widgets.table.default-page-size
@@ -104,8 +104,8 @@
widgets.table.rows
- - + + {{ 'widgets.table.use-row-style-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html index 02d51ee325..faad357026 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html @@ -62,8 +62,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -86,8 +86,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html index f8867b9f6a..c9c57d8843 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html @@ -41,7 +41,7 @@
widgets.table.columns
-
+
{{ 'widgets.table.display-entity-name' | translate }} @@ -49,7 +49,7 @@
-
+
{{ 'widgets.table.display-entity-label' | translate }} @@ -91,7 +91,7 @@ {{ 'widgets.table.display-pagination' | translate }} -
+
widgets.table.default-page-size
@@ -101,8 +101,8 @@
widgets.table.rows
- - + + {{ 'widgets.table.use-row-style-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html index 6c79eed622..165f4eb0f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html @@ -50,8 +50,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -74,8 +74,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html index 97cbe28ae9..fd34b66af6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html @@ -59,8 +59,8 @@
- - + + {{ 'widgets.table.use-cell-style-function' | translate }} @@ -83,8 +83,8 @@
- - + + {{ 'widgets.table.use-cell-content-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html index 170d460f92..d70ae9964b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html @@ -59,7 +59,7 @@ {{ 'widgets.table.display-pagination' | translate }} -
+
widgets.table.default-page-size
@@ -78,8 +78,8 @@ {{ 'widgets.table.hide-empty-lines' | translate }} - - + + {{ 'widgets.table.use-row-style-function' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html new file mode 100644 index 0000000000..71a9b60118 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html @@ -0,0 +1,89 @@ + + +
+
widgets.value-card.value-card-style
+ + + {{ valueCardLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.value-card.label' | translate }} + +
+ + + + +
+
+
+ + {{ 'widgets.value-card.icon' | translate }} + +
+ + + + + + + + +
+
+
+
widgets.value-card.value
+
+ + + + +
+
+
+ + {{ 'widgets.value-card.date' | translate }} + +
+ + + + + +
+
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts new file mode 100644 index 0000000000..6f76546ac1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts @@ -0,0 +1,200 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Injector } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + valueCardDefaultSettings, + ValueCardLayout, valueCardLayoutImages, + valueCardLayouts, valueCardLayoutTranslations +} from '@home/components/widget/lib/cards/value-card-widget.models'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { + DateFormatProcessor, + DateFormatSettings, + getLabel +} from '@home/components/widget/config/widget-settings.models'; + +@Component({ + selector: 'tb-value-card-widget-settings', + templateUrl: './value-card-widget-settings.component.html', + styleUrls: [] +}) +export class ValueCardWidgetSettingsComponent extends WidgetSettingsComponent { + + valueCardLayouts: ValueCardLayout[] = []; + + valueCardLayoutTranslationMap = valueCardLayoutTranslations; + valueCardLayoutImageMap = valueCardLayoutImages; + + horizontal = false; + + valueCardWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + datePreviewFn = this._datePreviewFn.bind(this); + + + get label(): string { + return getLabel(this.widgetConfig.config.datasources); + } + + get dateEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value; + return ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + } + + get iconEnabled(): boolean { + const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value; + return layout !== ValueCardLayout.simplified; + } + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.valueCardWidgetSettingsForm; + } + + protected onWidgetConfigSet(widgetConfig: WidgetConfigComponentData) { + const params = widgetConfig.typeParameters as any; + this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false; + this.valueCardLayouts = valueCardLayouts(this.horizontal); + } + + protected defaultSettings(): WidgetSettings { + return valueCardDefaultSettings(this.horizontal); + } + + protected onSettingsSet(settings: WidgetSettings) { + this.valueCardWidgetSettingsForm = this.fb.group({ + layout: [settings.layout, []], + + showLabel: [settings.showLabel, []], + labelFont: [settings.labelFont, []], + labelColor: [settings.labelColor, []], + + showIcon: [settings.showIcon, []], + iconSize: [settings.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.iconSizeUnit, []], + icon: [settings.icon, []], + iconColor: [settings.iconColor, []], + + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + showDate: [settings.showDate, []], + dateFormat: [settings.dateFormat, []], + dateFont: [settings.dateFont, []], + dateColor: [settings.dateColor, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['layout', 'showLabel', 'showIcon', 'showDate']; + } + + protected updateValidators(emitEvent: boolean) { + const layout: ValueCardLayout = this.valueCardWidgetSettingsForm.get('layout').value; + const showLabel: boolean = this.valueCardWidgetSettingsForm.get('showLabel').value; + const showIcon: boolean = this.valueCardWidgetSettingsForm.get('showIcon').value; + const showDate: boolean = this.valueCardWidgetSettingsForm.get('showDate').value; + + const dateEnabled = ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout); + const iconEnabled = layout !== ValueCardLayout.simplified; + + if (showLabel) { + this.valueCardWidgetSettingsForm.get('labelFont').enable(); + this.valueCardWidgetSettingsForm.get('labelColor').enable(); + } else { + this.valueCardWidgetSettingsForm.get('labelFont').disable(); + this.valueCardWidgetSettingsForm.get('labelColor').disable(); + } + + if (iconEnabled) { + this.valueCardWidgetSettingsForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.valueCardWidgetSettingsForm.get('iconSize').enable(); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').enable(); + this.valueCardWidgetSettingsForm.get('icon').enable(); + this.valueCardWidgetSettingsForm.get('iconColor').enable(); + } else { + this.valueCardWidgetSettingsForm.get('iconSize').disable(); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').disable(); + this.valueCardWidgetSettingsForm.get('icon').disable(); + this.valueCardWidgetSettingsForm.get('iconColor').disable(); + } + } else { + this.valueCardWidgetSettingsForm.get('showIcon').disable({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('iconSize').disable(); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').disable(); + this.valueCardWidgetSettingsForm.get('icon').disable(); + this.valueCardWidgetSettingsForm.get('iconColor').disable(); + } + + if (dateEnabled) { + this.valueCardWidgetSettingsForm.get('showDate').enable({emitEvent: false}); + if (showDate) { + this.valueCardWidgetSettingsForm.get('dateFormat').enable(); + this.valueCardWidgetSettingsForm.get('dateFont').enable(); + this.valueCardWidgetSettingsForm.get('dateColor').enable(); + } else { + this.valueCardWidgetSettingsForm.get('dateFormat').disable(); + this.valueCardWidgetSettingsForm.get('dateFont').disable(); + this.valueCardWidgetSettingsForm.get('dateColor').disable(); + } + } else { + this.valueCardWidgetSettingsForm.get('showDate').disable({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('dateFormat').disable(); + this.valueCardWidgetSettingsForm.get('dateFont').disable(); + this.valueCardWidgetSettingsForm.get('dateColor').disable(); + } + this.valueCardWidgetSettingsForm.get('showIcon').updateValueAndValidity({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('showDate').updateValueAndValidity({emitEvent: false}); + this.valueCardWidgetSettingsForm.get('labelFont').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('labelColor').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('iconSize').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('iconSizeUnit').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('icon').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('iconColor').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('dateFormat').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('dateFont').updateValueAndValidity({emitEvent}); + this.valueCardWidgetSettingsForm.get('dateColor').updateValueAndValidity({emitEvent}); + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, true); + } + + private _datePreviewFn(): string { + const dateFormat: DateFormatSettings = this.valueCardWidgetSettingsForm.get('dateFormat').value; + const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat); + processor.update(Date.now()); + return processor.formatted; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html index 96ff4d8559..a424b61606 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html @@ -32,8 +32,8 @@
- - + + {{ 'widgets.chart.show-line' | translate }} @@ -65,8 +65,8 @@
- - + + {{ 'widgets.chart.show-points' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html index 6f08b5f3f7..be2ae39255 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.html @@ -17,7 +17,7 @@ -->
- +
{{ thresholdText() }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss index 150d516894..81f5d17bc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-threshold.component.scss @@ -29,6 +29,9 @@ display: flex; flex-direction: row; align-items: stretch; + .mat-content { + overflow: hidden; + } .tb-threshold-header { flex: 1; display: flex; @@ -36,6 +39,7 @@ gap: 16px; align-items: center; padding-left: 16px; + overflow: hidden; .mat-divider-vertical { height: 100%; } @@ -47,6 +51,9 @@ font-weight: 400; line-height: 16px; letter-spacing: 0.15px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .mat-expansion-indicator { margin-right: 22px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html index 5dd7eaa2dc..4ea648c3b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html @@ -77,8 +77,8 @@
widget-config.legend
- - + + {{ 'widget-config.legend' | translate }} @@ -119,8 +119,8 @@
widgets.chart.ticks
- - + + {{ 'widgets.chart.ticks' | translate }} @@ -171,8 +171,8 @@
widgets.chart.ticks
- - + + {{ 'widgets.chart.ticks' | translate }} @@ -234,8 +234,8 @@
widgets.chart.tooltip
- - + + {{ 'widgets.chart.tooltip' | translate }} @@ -274,8 +274,8 @@
widgets.chart.comparison-settings
- - + + {{ 'widgets.chart.enable-comparison' | translate }} @@ -350,8 +350,8 @@
widgets.chart.custom-legend-settings
- - + + {{ 'widgets.chart.enable-custom-legend' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html new file mode 100644 index 0000000000..42c4472cca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.html @@ -0,0 +1,87 @@ + +
+
widgets.background.background-settings
+
+
+
widgets.background.background
+ + + {{ backgroundTypeTranslationsMap.get(type) | translate }} + + +
+ +
+
widgets.background.image-url
+ + + +
+
+
widgets.color.color
+ + +
+
+
+
widgets.background.overlay
+ + {{ 'widgets.background.enable-overlay' | translate }} + +
+
widgets.color.color
+ + +
+
+
widgets.background.blur
+ + +
px
+
+
+
+
+
+ widgets.background.preview +
+
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss new file mode 100644 index 0000000000..258117512a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.scss @@ -0,0 +1,73 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import '../../../../../../../../scss/constants'; + +.tb-background-settings-panel { + width: 620px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-background-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-background-settings-preview { + flex: 1; + background: rgba(0, 0, 0, 0.04); + display: flex; + flex-direction: column; + padding: 12px 16px 24px 16px; + align-items: center; + gap: 12px; + } + .tb-background-settings-preview-title { + align-self: stretch; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + color: rgba(0, 0, 0, 0.38); + } + .tb-background-settings-preview-box { + position: relative; + width: 136px; + height: 118px; + border-radius: 2.666px; + } + .tb-background-settings-preview-overlay { + position: absolute; + border-radius: 2.666px; + top: 7.998px; + bottom: 7.998px; + left: 7.998px; + right: 7.998px; + } + .tb-background-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts new file mode 100644 index 0000000000..465d3383e2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings-panel.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { + backgroundStyle, + overlayStyle, + BackgroundSettings, + BackgroundType, + backgroundTypeTranslations, ComponentStyle +} from '@home/components/widget/config/widget-settings.models'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-background-settings-panel', + templateUrl: './background-settings-panel.component.html', + providers: [], + styleUrls: ['./background-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class BackgroundSettingsPanelComponent extends PageComponent implements OnInit { + + @Input() + backgroundSettings: BackgroundSettings; + + @Input() + popover: TbPopoverComponent; + + @Output() + backgroundSettingsApplied = new EventEmitter(); + + backgroundType = BackgroundType; + + backgroundTypes = Object.keys(BackgroundType) as BackgroundType[]; + + backgroundTypeTranslationsMap = backgroundTypeTranslations; + + backgroundSettingsFormGroup: UntypedFormGroup; + + backgroundStyle: ComponentStyle = {}; + overlayStyle: ComponentStyle = {}; + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + } + + ngOnInit(): void { + this.backgroundSettingsFormGroup = this.fb.group( + { + type: [this.backgroundSettings?.type, []], + imageBase64: [this.backgroundSettings?.imageBase64, []], + imageUrl: [this.backgroundSettings?.imageUrl, []], + color: [this.backgroundSettings?.color, []], + overlay: this.fb.group({ + enabled: [this.backgroundSettings?.overlay?.enabled, []], + color: [this.backgroundSettings?.overlay?.color, []], + blur: [this.backgroundSettings?.overlay?.blur, []] + }) + } + ); + this.backgroundSettingsFormGroup.get('type').valueChanges.subscribe(() => { + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + this.backgroundSettingsFormGroup.get('overlay').get('enabled').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.backgroundSettingsFormGroup.valueChanges.subscribe(() => { + this.updateBackgroundStyle(); + }); + this.updateValidators(); + this.updateBackgroundStyle(); + } + + cancel() { + this.popover?.hide(); + } + + applyColorSettings() { + const backgroundSettings = this.backgroundSettingsFormGroup.getRawValue(); + this.backgroundSettingsApplied.emit(backgroundSettings); + } + + private updateValidators() { + const overlayEnabled: boolean = this.backgroundSettingsFormGroup.get('overlay').get('enabled').value; + if (overlayEnabled) { + this.backgroundSettingsFormGroup.get('overlay').get('color').enable(); + this.backgroundSettingsFormGroup.get('overlay').get('blur').enable(); + } else { + this.backgroundSettingsFormGroup.get('overlay').get('color').disable(); + this.backgroundSettingsFormGroup.get('overlay').get('blur').disable(); + } + this.backgroundSettingsFormGroup.get('overlay').get('color').updateValueAndValidity({emitEvent: false}); + this.backgroundSettingsFormGroup.get('overlay').get('blur').updateValueAndValidity({emitEvent: false}); + } + + private updateBackgroundStyle() { + const background: BackgroundSettings = this.backgroundSettingsFormGroup.value; + this.backgroundStyle = backgroundStyle(background); + this.overlayStyle = overlayStyle(background.overlay); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html new file mode 100644 index 0000000000..e9e1b99b0e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.html @@ -0,0 +1,30 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss new file mode 100644 index 0000000000..6f73fbffa4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.scss @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +button.mat-mdc-button-base.tb-box-button.tb-background-settings { + padding: 0; + .mat-mdc-button-persistent-ripple { + z-index: 2; + } + .tb-color-preview { + width: 38px; + min-width: 38px; + height: 38px; + &.box { + .tb-color-result { + &:after { + border: none; + } + } + .tb-color-overlay { + position: absolute; + border-radius: 3px; + top: 4px; + bottom: 4px; + left: 4px; + right: 4px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts new file mode 100644 index 0000000000..f8162575a3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/background-settings.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef, ViewEncapsulation } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { + BackgroundSettings, + backgroundStyle, + BackgroundType, + ComponentStyle, + overlayStyle +} from '@home/components/widget/config/widget-settings.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { + BackgroundSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/background-settings-panel.component'; + +@Component({ + selector: 'tb-background-settings', + templateUrl: './background-settings.component.html', + styleUrls: ['./background-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BackgroundSettingsComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class BackgroundSettingsComponent implements OnInit, ControlValueAccessor { + + @Input() + disabled: boolean; + + backgroundType = BackgroundType; + + modelValue: BackgroundSettings; + + backgroundStyle: ComponentStyle = {}; + + overlayStyle: ComponentStyle = {}; + + private propagateChange = null; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef) {} + + ngOnInit(): void { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.updateBackgroundStyle(); + } + + writeValue(value: BackgroundSettings): void { + this.modelValue = value; + this.updateBackgroundStyle(); + } + + openBackgroundSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + backgroundSettings: this.modelValue + }; + const backgroundSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, BackgroundSettingsPanelComponent, 'left', true, null, + ctx, + {}, + {}, {}, true); + backgroundSettingsPanelPopover.tbComponentRef.instance.popover = backgroundSettingsPanelPopover; + backgroundSettingsPanelPopover.tbComponentRef.instance.backgroundSettingsApplied.subscribe((backgroundSettings) => { + backgroundSettingsPanelPopover.hide(); + this.modelValue = backgroundSettings; + this.updateBackgroundStyle(); + this.propagateChange(this.modelValue); + }); + } + } + + private updateBackgroundStyle() { + if (!this.disabled) { + this.backgroundStyle = backgroundStyle(this.modelValue); + this.overlayStyle = overlayStyle(this.modelValue.overlay); + } else { + this.backgroundStyle = {}; + this.overlayStyle = {}; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html index eb2bbd6711..52bd5907bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html @@ -17,7 +17,7 @@ -->
- +
{{ label }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts index e538653703..d75c4a5fc9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts @@ -21,14 +21,14 @@ import { Directive, ElementRef, forwardRef, - Input, + Input, OnChanges, OnDestroy, OnInit, - QueryList, + QueryList, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { Observable, Subject } from 'rxjs'; +import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; import { map, share, startWith, takeUntil } from 'rxjs/operators'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; @@ -73,7 +73,7 @@ export class ImageCardsSelectOptionDirective { ], encapsulation: ViewEncapsulation.None }) -export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, AfterContentInit, OnDestroy { +export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, OnChanges, AfterContentInit, OnDestroy { @ContentChildren(ImageCardsSelectOptionDirective) imageCardsSelectOptions: QueryList; @@ -107,20 +107,33 @@ export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, private _destroyed = new Subject(); + private _colsChanged = new BehaviorSubject(null); + constructor(private breakpointObserver: BreakpointObserver) { this.valueFormControl = new UntypedFormControl(''); } ngOnInit(): void { const gridColumns = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? this.colsLtMd : this.cols; - this.cols$ = this.breakpointObserver - .observe(MediaBreakpoints['lt-md']).pipe( - map((state) => state.matches ? this.colsLtMd : this.cols), + this.cols$ = combineLatest({state: this.breakpointObserver + .observe(MediaBreakpoints['lt-md']), colsChanged: this._colsChanged.asObservable()}).pipe( + map((data) => data.state.matches ? this.colsLtMd : this.cols), startWith(gridColumns), share() ); } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (['cols', 'colsLtMd'].includes(propName)) { + this._colsChanged.next(null); + } + } + } + } + ngAfterContentInit(): void { this.imageCardsSelectOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { this.syncImageCardsSelectOptions(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html index 10bfa058da..349bc0cd04 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html @@ -38,7 +38,7 @@
-
+
legend.show-values
{{ 'legend.min-option' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index 3cc771e94f..726aed776e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -17,7 +17,7 @@ -->
- + {{ 'widgets.value-source.predefined-value' | translate }} @@ -32,7 +32,7 @@
-
+
widgets.value-source.source-entity-alias
-
+
widgets.value-source.source-entity-attribute
{{ 'widgets.input-widgets.datakey-value-type-json' | translate }} + + {{ 'widgets.input-widgets.datakey-value-type-color' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index c03b4d3ee4..4798dbe679 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -287,6 +287,13 @@ import { DateFormatSelectComponent } from '@home/components/widget/lib/settings/ import { DateFormatSettingsPanelComponent } from '@home/components/widget/lib/settings/common/date-format-settings-panel.component'; +import { BackgroundSettingsComponent } from '@home/components/widget/lib/settings/common/background-settings.component'; +import { + BackgroundSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/background-settings-panel.component'; +import { + ValueCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/value-card-widget-settings.component'; @NgModule({ declarations: [ @@ -399,7 +406,10 @@ import { ColorSettingsPanelComponent, CssUnitSelectComponent, DateFormatSelectComponent, - DateFormatSettingsPanelComponent + DateFormatSettingsPanelComponent, + BackgroundSettingsComponent, + BackgroundSettingsPanelComponent, + ValueCardWidgetSettingsComponent ], imports: [ CommonModule, @@ -516,7 +526,10 @@ import { ColorSettingsPanelComponent, CssUnitSelectComponent, DateFormatSelectComponent, - DateFormatSettingsPanelComponent + DateFormatSettingsPanelComponent, + BackgroundSettingsComponent, + BackgroundSettingsPanelComponent, + ValueCardWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -587,5 +600,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type - - +
+ + +
@@ -48,18 +50,18 @@
-
+
{{ 'widget-config.display-icon' | translate }}
+ + + - - - @@ -247,7 +249,7 @@
widget-config.limits
-
+
widget-config.data-page-size
@@ -258,19 +260,19 @@
widget-config.data-settings
-
+
widget-config.units
-
+
widget-config.decimals
-
+
widget-config.no-data-display-message
@@ -282,6 +284,7 @@ [aliasController]="aliasController" [dashboard]="dashboard" [widget]="widget" + [widgetConfig]="modelValue" formControlName="settings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 91b3368ad0..701bfec099 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -20,16 +20,36 @@ .tb-widget-config { display: flex; flex-direction: column; - gap: 16px; + gap: 8px; .tb-widget-config-header { - padding: 24px 24px 8px; - height: 56px; + padding: 24px 24px 0; display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; + gap: 12px; + flex-direction: column-reverse; + align-items: flex-start; + @media #{$mat-gt-sm} { + gap: 0; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + .tb-widget-config-header-components { + width: 100%; + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } } .tb-widget-config-content { + & > .mat-content { + padding-top: 8px; + @media #{$mat-xs} { + padding-left: 8px; + padding-right: 8px; + } + } flex: 1; overflow: auto; & > div { @@ -39,6 +59,9 @@ display: flex; flex-direction: column; gap: 16px; + @media #{$mat-xs} { + gap: 8px; + } } } .tb-basic-mode-directive-error { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 6601aa96db..43b54884ec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -19,7 +19,6 @@ [fullscreenBackgroundStyle]="dashboardStyle" [fullscreenBackgroundImage]="backgroundImage" (fullscreenChanged)="onFullscreenChanged($event)" - fxLayout="column" class="tb-widget" [ngClass]="{ 'tb-highlighted': isHighlighted(widget), @@ -32,8 +31,11 @@ (mousedown)="onMouseDown($event)" (click)="onClicked($event)" (contextmenu)="onContextMenu($event)"> -
-
+
+
-
- + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss index a364189372..52caeb2a5c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -14,9 +14,14 @@ * limitations under the License. */ -tb-widget.tb-widget { - position: relative; - height: 100%; +.tb-widget-container { + position: absolute; + inset: 0; +} + +.tb-widget { + position: absolute; + inset: 0; margin: 0; overflow: hidden; outline: none; @@ -25,15 +30,27 @@ tb-widget.tb-widget { } div.tb-widget { - position: relative; - height: 100%; - margin: 0; - overflow: hidden; - outline: none; - - transition: all .2s ease-in-out; + display: flex; + flex-direction: column; + .tb-widget-header { + display: flex; + flex-direction: row; + place-content: flex-start space-between; + align-items: flex-start; + &-absolute { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 1; + } + } .tb-widget-title { + display: flex; + flex-direction: column; + place-content: flex-start center; + align-items: flex-start; max-height: 65px; padding-top: 5px; padding-left: 5px; @@ -63,6 +80,10 @@ div.tb-widget { } .tb-widget-actions { + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: center; z-index: 19; margin: 5px 0 0; @@ -104,13 +125,11 @@ div.tb-widget { } .tb-widget-content { + flex: 1; + position: relative; &.tb-no-interaction { pointer-events: none; } - tb-widget { - position: relative; - width: 100%; - } } &.tb-highlighted { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 7e9b9cb6e7..c276999d83 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -409,6 +409,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI elem.classList.add(this.widgetContext.widgetNamespace); this.widgetType = this.widgetInfo.widgetTypeFunction; this.typeParameters = this.widgetInfo.typeParameters; + this.widgetContext.absoluteHeader = this.typeParameters.absoluteHeader; if (!this.widgetType) { this.widgetTypeInstance = {}; diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 8d55b4bc6a..fd427923f7 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -76,7 +76,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 65e7df1e90..8210f9a533 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -23,7 +23,6 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; import { Device, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; import { MatStepper, StepperOrientation } from '@angular/material/stepper'; -import { BaseData, HasId } from '@shared/models/base-data'; import { EntityType } from '@shared/models/entity-type.models'; import { Observable, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; @@ -40,7 +39,7 @@ import { HttpErrorResponse } from '@angular/common/http'; templateUrl: './device-wizard-dialog.component.html', styleUrls: ['./device-wizard-dialog.component.scss'] }) -export class DeviceWizardDialogComponent extends DialogComponent { +export class DeviceWizardDialogComponent extends DialogComponent { @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper; @@ -64,7 +63,7 @@ export class DeviceWizardDialogComponent extends DialogComponent, protected router: Router, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private deviceService: DeviceService, private breakpointObserver: BreakpointObserver, private fb: FormBuilder) { @@ -121,7 +120,7 @@ export class DeviceWizardDialogComponent extends DialogComponent this.dialogRef.close(true) + (device) => this.dialogRef.close(device) ); } } @@ -137,7 +136,7 @@ export class DeviceWizardDialogComponent extends DialogComponent> { + private createDevice(): Observable { const device: Device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss index fc9df865a1..dbba5e78a9 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -49,7 +49,7 @@ &.tb-active { background-color: rgba(255, 255, 255, .15); } - mat-icon { + .mat-icon { margin-right: 8px; margin-left: 0; min-width: 1.125rem; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index b16e880e02..18c8ccd19e 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -265,6 +265,8 @@ export class WidgetContext { hiddenData?: Array<{data: DataSet}>; timeWindow?: WidgetTimewindow; + absoluteHeader?: boolean; + hideTitlePanel = false; widgetTitle?: string; diff --git a/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts new file mode 100644 index 0000000000..9fcaecddfd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/account/account-routing.module.ts @@ -0,0 +1,61 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { inject, NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { RouterTabsComponent } from '@home/components/router-tabs.component'; +import { Authority } from '@shared/models/authority.enum'; +import { securityRoutes } from '@home/pages/security/security-routing.module'; +import { profileRoutes } from '@home/pages/profile/profile-routing.module'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +const routes: Routes = [ + { + path: 'account', + component: RouterTabsComponent, + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + breadcrumb: { + label: 'account.account', + icon: 'account_circle' + }, + useChildrenRoutesForTabs: true, + }, + resolve: { + replaceUrl: () => getCurrentAuthState(inject(Store)).forceFullscreen + }, + children: [ + { + path: '', + children: [], + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], + redirectTo: '/account/profile', + } + }, + ...profileRoutes, + ...securityRoutes + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class AccountRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/account/account.module.ts b/ui-ngx/src/app/modules/home/pages/account/account.module.ts new file mode 100644 index 0000000000..df178607ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/account/account.module.ts @@ -0,0 +1,28 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { NgModule } from '@angular/core'; +import { AccountRoutingModule } from '@home/pages/account/account-routing.module'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + declarations: [ ], + imports: [ + CommonModule, + AccountRoutingModule + ] +}) +export class AccountModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html new file mode 100644 index 0000000000..71a8364134 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.html @@ -0,0 +1,402 @@ + + +

{{ dialogTitle }}

+ + + +
+
+
+ + + {{ deviceTransportTypeTranslationMap.get(BasicTransportType.HTTP) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.MQTT) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.COAP) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.SNMP) | translate }} + + + {{ deviceTransportTypeTranslationMap.get(DeviceTransportType.LWM2M) | translate }} + + +
+ + +
device.connectivity.use-following-instructions
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
device.connectivity.install-curl-windows
+
+ + +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+
+
+ +
+ + +
+ +
device.connectivity.use-following-instructions
+ + + + + Windows + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+ +
+ + +
+
+
+ + + + Docker + + +
+ + +
+
+
+
+
+
+ +
device.connectivity.use-following-instructions
+ + + + + MacOS + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + Linux + + +
+
+
device.connectivity.install-necessary-client-tools
+
+ + +
+
+ + +
+
+
+ + + + Docker + + +
+ + +
+
+
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+
+
+
+
device.state
+
+ {{ (status ? 'device.active' : 'device.inactive') | translate }} +
+
+
attribute.latest-telemetry
+
+
+
device.time
+
attribute.key
+
attribute.value
+
+
+
+
{{ telemetry.lastUpdateTs | date: 'yyyy-MM-dd HH:mm:ss' }}
+
{{ telemetry.key }}
+
{{ telemetry.value }}
+
+
+
+
+
+
+
+ {{ 'action.dont-show-again' | translate}} + + +
+ +
+ + + {{ 'device.connectivity.loading-check-connectivity-command' | translate }} + +
+
+ +
+
+
attribute.no-latest-telemetry
+
+
+ + +
+
+
device.connectivity.execute-following-command
+ + {{ cmd.noSecLabel }} + {{ cmd.secLabel }} + +
+ + + + + +
+ +
+ + + + +
+
+
+
+ + + + diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss new file mode 100644 index 0000000000..348fa01a26 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.scss @@ -0,0 +1,177 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import "../../../../../scss/constants"; + +:host { + height: 100%; + max-height: 100vh; + display: grid; + grid-template-rows: min-content minmax(auto, 1fr) min-content; + + .tb-loader { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + height: 300px; + max-height: 100%; + + .label { + margin-bottom: 0; + text-align: center; + } + } + + .status { + margin-left: 12px; + border-radius: 12px; + height: 24px; + line-height: 24px; + padding: 0 8px; + width: fit-content; + color: #198038; + background-color: rgba(25, 128, 56, 0.08); + font-size: 14px; + + &.inactive { + color: #d12730; + background-color: rgba(209, 39, 48, 0.08); + } + } + + .tb-hint-instruction { + border-radius: 6px; + background-color: rgba(48, 86, 128, 0.04); + padding: 6px 16px; + + .content { + vertical-align: middle; + } + } + + .tb-font-14 { + font-size: 14px; + } + + .tb-flex-1 { + flex: 1; + } + + .tb-form-table-body { + max-height: 108px; + min-height: 108px; + overflow-y: auto; + scrollbar-gutter: stable; + + .tb-form-table-row { + min-height: 38px; + } + } + + .tb-no-data-available { + .tb-no-data-bg { + min-height: 68px; + } + } + + .tb-install-instruction-text { + min-height: 42px; + } + + @media #{$mat-sm} { + width: 470px; + } + + @media #{$mat-gt-sm} { + width: 720px; + } +} + +:host-context(.mat-mdc-dialog-container) { + .tb-dialog-actions { + display: flex; + gap: 8px; + padding: 8px 16px; + } + + .mat-mdc-dialog-content { + max-height: 80vh; + padding: 16px; + } +} + +:host ::ng-deep { + .tb-markdown-view { + .tb-command-code { + .code-wrapper { + padding: 0; + pre[class*=language-] { + margin: 0; + background: #F3F6FA; + border-color: #305680; + padding-right: 38px; + overflow: scroll; + padding-bottom: 4px; + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + } + } + button.clipboard-btn { + right: -2px; + p { + color: #305680; + } + p, div { + background-color: #F3F6FA; + } + div { + img { + display: none; + } + &:after { + content: ""; + position: initial; + display: block; + width: 18px; + height: 18px; + background: #305680; + mask-image: url(/assets/copy-code-icon.svg); + mask-repeat: no-repeat; + } + } + } + } + } + .mdc-button__label > span { + .mat-icon { + vertical-align: text-bottom; + box-sizing: initial; + } + } + + .tabs-icon { + margin-right: 8px; + } + + .tb-form-panel.tb-tab-body { + padding: 16px 0 0; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts new file mode 100644 index 0000000000..7516e0f3e1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-check-connectivity-dialog.component.ts @@ -0,0 +1,222 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DeviceService } from '@core/http/device.service'; +import { + AttributeData, + AttributeScope, + AttributesSubscriptionCmd, + LatestTelemetry, + TelemetrySubscriber +} from '@shared/models/telemetry/telemetry.models'; +import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; +import { selectPersistDeviceStateToTelemetry } from '@core/auth/auth.selectors'; +import { take } from 'rxjs/operators'; +import { + BasicTransportType, + DeviceTransportType, + deviceTransportTypeTranslationMap, + NetworkTransportType, + PublishTelemetryCommand +} from '@shared/models/device.models'; +import { ActionPreferencesPutUserSettings } from '@core/auth/auth.actions'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { getOS } from '@core/utils'; + +export interface DeviceCheckConnectivityDialogData { + deviceId: EntityId; + afterAdd: boolean; +} + +@Component({ + selector: 'tb-device-check-connectivity-dialog', + templateUrl: './device-check-connectivity-dialog.component.html', + styleUrls: ['./device-check-connectivity-dialog.component.scss'] +}) +export class DeviceCheckConnectivityDialogComponent extends + DialogComponent implements OnInit, OnDestroy { + + loadedCommand = false; + + status: boolean; + + latestTelemetry: Array = []; + + commands: PublishTelemetryCommand; + + allowTransportType = new Set(); + selectTransportType: NetworkTransportType; + + BasicTransportType = BasicTransportType; + DeviceTransportType = DeviceTransportType; + deviceTransportTypeTranslationMap = deviceTransportTypeTranslationMap; + + showDontShowAgain: boolean; + dialogTitle: string; + closeButtonLabel: string; + + notShowAgain = false; + + httpTabIndex = 0; + mqttTabIndex = 0; + coapTabIndex = 0; + + private telemetrySubscriber: TelemetrySubscriber; + + private currentTime = Date.now(); + + private transportTypes = [...Object.keys(BasicTransportType), ...Object.keys(DeviceTransportType)] as Array; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) private data: DeviceCheckConnectivityDialogData, + public dialogRef: MatDialogRef, + private deviceService: DeviceService, + private telemetryWsService: TelemetryWebsocketService, + private zone: NgZone) { + super(store, router, dialogRef); + + if (this.data.afterAdd) { + this.dialogTitle = 'device.connectivity.device-created-check-connectivity'; + this.closeButtonLabel = 'action.skip'; + this.showDontShowAgain = true; + } else { + this.dialogTitle = 'device.connectivity.check-connectivity'; + this.closeButtonLabel = 'action.close'; + this.showDontShowAgain = false; + } + } + + ngOnInit() { + this.loadCommands(); + this.subscribeToLatestTelemetry(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.telemetrySubscriber?.complete(); + this.telemetrySubscriber?.unsubscribe(); + } + + close(): void { + if (this.notShowAgain && this.showDontShowAgain) { + this.store.dispatch(new ActionPreferencesPutUserSettings({ notDisplayConnectivityAfterAddDevice: true })); + this.dialogRef.close(null); + } else { + this.dialogRef.close(null); + } + } + + createMarkDownCommand(commands: string | string[]): string { + if (Array.isArray(commands)) { + const formatCommands: Array = []; + commands.forEach(command => formatCommands.push(this.createMarkDownSingleCommand(command))); + return formatCommands.join(`\n
\n\n`); + } else { + return this.createMarkDownSingleCommand(commands); + } + } + + private createMarkDownSingleCommand(command: string): string { + return '```bash\n' + + command + + '{:copy-code}\n' + + '```'; + } + + private loadCommands() { + this.deviceService.getDevicePublishTelemetryCommands(this.data.deviceId.id).subscribe( + commands => { + this.commands = commands; + const commandsProtocols = Object.keys(commands); + this.transportTypes.forEach(transport => { + const findCommand = commandsProtocols.find(item => item.toUpperCase().startsWith(transport)); + if (findCommand) { + this.allowTransportType.add(transport); + } + }); + this.selectTransportType = this.allowTransportType.values().next().value; + this.selectTabIndexForUserOS(); + this.loadedCommand = true; + } + ); + } + + private subscribeToLatestTelemetry() { + this.store.pipe(select(selectPersistDeviceStateToTelemetry)).pipe( + take(1) + ).subscribe(persistToTelemetry => { + this.telemetrySubscriber = TelemetrySubscriber.createEntityAttributesSubscription( + this.telemetryWsService, this.data.deviceId, LatestTelemetry.LATEST_TELEMETRY, this.zone); + if (!persistToTelemetry) { + const subscriptionCommand = new AttributesSubscriptionCmd(); + subscriptionCommand.entityType = this.data.deviceId.entityType as EntityType; + subscriptionCommand.entityId = this.data.deviceId.id; + subscriptionCommand.scope = AttributeScope.SERVER_SCOPE; + subscriptionCommand.keys = 'active'; + this.telemetrySubscriber.subscriptionCommands.push(subscriptionCommand); + } + + this.telemetrySubscriber.subscribe(); + this.telemetrySubscriber.attributeData$().subscribe( + (data) => { + const telemetry = data.reduce>((accumulator, item) => { + if (item.key === 'active') { + this.status = coerceBooleanProperty(item.value); + } else if (item.lastUpdateTs > this.currentTime) { + accumulator.push(item); + } + return accumulator; + }, []); + this.latestTelemetry = telemetry.sort((a, b) => b.lastUpdateTs - a.lastUpdateTs); + } + ); + }); + } + + private selectTabIndexForUserOS() { + const currentOS = getOS(); + switch (currentOS) { + case 'linux': + case 'android': + this.httpTabIndex = 2; + this.mqttTabIndex = 2; + this.coapTabIndex = 1; + break; + case 'macos': + case 'ios': + this.httpTabIndex = 1; + this.mqttTabIndex = 1; + break; + case 'windows': + this.httpTabIndex = 0; + this.mqttTabIndex = 0; + break; + default: + this.mqttTabIndex = this.commands.mqtt?.docker ? 3 : 0; + this.coapTabIndex = this.commands.coap?.docker ? 2 : 1; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 244b1c000b..0b1ef5225c 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -46,6 +46,12 @@ [fxShow]="!isEdit"> {{ ((deviceScope === 'customer_user' || deviceScope === 'edge_customer_user') ? 'device.view-credentials' : 'device.manage-credentials') | translate }} + -
-
-
-
-
- cloud_upload - image-input.drop-image-or - - +
+
+
{{ 'dashboard.empty-image' | translate }}
+
-
-
-
-
+
+
+ cloud_upload +
+ image-input.drag-and-drop +
+ image-input.or + + +
+
+
+
+ +
dashboard.maximum-upload-file-size
diff --git a/ui-ngx/src/app/shared/components/image-input.component.scss b/ui-ngx/src/app/shared/components/image-input.component.scss index a776bc8cd7..b7d05bef8d 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.scss +++ b/ui-ngx/src/app/shared/components/image-input.component.scss @@ -15,9 +15,8 @@ */ @import "../../../scss/constants"; -$containerHeight: 120px !default; -$previewContainerWidth: 168px !default; -$previewSize: 96px !default; +$containerHeight: 96px !default; +$previewSize: 78px !default; :host { @@ -31,30 +30,39 @@ $previewSize: 96px !default; } .tb-image-select-container { - position: relative; width: 100%; height: $containerHeight; + display: flex; + gap: 12px; + align-items: center; } .image-container { - position: relative; - float: left; - height: $containerHeight; - padding: 12px; - margin-right: 8px; - background: rgba(0, 0, 0, 0.03); + background: #F3F6FA; border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.05); + padding: 8px 12px 8px 8px; + display: flex; + align-items: center; + gap: 12px; + &.disabled { + padding: 8px; + } } - .image-content-container { - background: #FFFFFF; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 4px; - padding-left: 8px; + .tb-image-preview-container { + width: $previewSize; height: $previewSize; - &.no-padding { - padding-left: 0px; - } + border: 1px solid rgba(0, 0, 0, 0.12); + background: #fff; + display: flex; + align-items: center; + justify-content: center; + } + + .tb-image-preview-text { + font-size: 14px; + text-align: center; } .tb-image-preview { @@ -64,57 +72,18 @@ $previewSize: 96px !default; max-height: $previewSize - 2px; } - .tb-image-preview-container { - position: relative; - float: left; - width: $previewSize; - height: $previewSize; - margin-top: -1px; - margin-bottom: -1px; - border: 1px solid rgba(0, 0, 0, 0.54); - - div { - width: 100%; - font-size: 18px; - text-align: center; - } - - div, - .tb-image-preview { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - } - - .tb-image-clear-container { - position: relative; - float: right; - height: $previewSize; - display: flex; - align-items: center; - &.full-height { - height: $containerHeight; - } - } - .file-input { display: none; } .tb-flow-drop { - position: relative; - height: $containerHeight; + height: 100%; + flex: 1; overflow: hidden; border: 2px dashed rgba(0, 0, 0, 0.2); border-radius: 4px; box-sizing: border-box; - &.float-left { - float: left; - } - .upload-label { width: 100%; height: 100%; @@ -123,11 +92,29 @@ $previewSize: 96px !default; flex-direction: row; justify-content: center; align-items: center; - font-size: 16px; - color: rgba(0, 0, 0, 0.54); - text-align: center; + gap: 8px; .mat-icon { - margin-right: 17px; + color: rgba(0,0,0,0.12); + } + .upload-text-area { + display: flex; + flex-direction: column; + align-items: center; + font-size: 16px; + line-height: 24px; + color: rgba(0, 0, 0, 0.54); + text-align: center; + .hide-xs { + @media #{$mat-xs} { + display: none; + } + } + } + .upload-button-area { + display: flex; + justify-content: center; + align-items: flex-start; + gap: 6px; } } } @@ -138,13 +125,14 @@ $previewSize: 96px !default; } :host ::ng-deep { - button.browse-file { + button.mat-mdc-button.mat-mdc-button-base.browse-file { padding: 0; + min-width: 0; + height: 24px; font-size: 16px; label { display: block; cursor: pointer; - padding: 0 16px; } } } diff --git a/ui-ngx/src/app/shared/components/image-input.component.ts b/ui-ngx/src/app/shared/components/image-input.component.ts index 026b64a413..f5b80aa1e0 100644 --- a/ui-ngx/src/app/shared/components/image-input.component.ts +++ b/ui-ngx/src/app/shared/components/image-input.component.ts @@ -65,9 +65,6 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @Input() disabled: boolean; - @Input() - showClearButton = true; - @Input() showPreview = true; diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss index 6f38e6a6a5..f6f24e2608 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss @@ -51,7 +51,7 @@ margin: 2px; line-height: 24px; - mat-icon { + .mat-icon { width: 24px; height: 24px; @@ -93,7 +93,7 @@ margin: 0; line-height: 28px; - mat-icon { + .mat-icon { width: 24px; height: 24px; font-size: 24px; diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.scss b/ui-ngx/src/app/shared/components/time/timewindow.component.scss index 695362197d..af3feec6eb 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.scss +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.scss @@ -17,6 +17,9 @@ min-width: 48px; margin: 8px 0; max-width: 100%; + &.no-margin { + margin: 0; + } .mdc-button { max-width: 100%; } diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index f9b40daac1..ff39e54fc7 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, ElementRef, - forwardRef, + forwardRef, HostBinding, Injector, Input, StaticProvider, @@ -83,6 +83,11 @@ export class TimewindowComponent implements ControlValueAccessor { return this.historyOnlyValue; } + @HostBinding('class.no-margin') + @Input() + @coerceBoolean() + noMargin = false; + @Input() @coerceBoolean() forAllTimeEnabled = false; diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.html b/ui-ngx/src/app/shared/components/toggle-header.component.html index d7ed76de90..7aa391b3b4 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.html +++ b/ui-ngx/src/app/shared/components/toggle-header.component.html @@ -15,14 +15,33 @@ limitations under the License. --> - - {{ option.name }} - + +
+ + {{ option.name }} + +
+ diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.scss b/ui-ngx/src/app/shared/components/toggle-header.component.scss index 6a6785c11b..ac24f7bf29 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.scss +++ b/ui-ngx/src/app/shared/components/toggle-header.component.scss @@ -17,8 +17,36 @@ @import "../../../theme"; @import "../../../scss/constants"; +:host { + max-width: 100%; + display: grid; + grid-template-columns: min-content minmax(auto, 1fr) min-content; + .tb-toggle-header-pagination-button { + display: none; + } + &.tb-toggle-header-pagination-controls-enabled { + .tb-toggle-header-pagination-button { + display: block; + } + } + .tb-toggle-container, .tb-toggle-header-select { + display: inline-grid; + grid-column: 2; + overflow: hidden; + } + .tb-toggle-container { + &.tb-disable-pagination { + overflow: visible; + } + } + .tb-toggle-header { + transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); + } +} + :host ::ng-deep { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + overflow: visible; width: 100%; border-radius: 100px; height: 32px; @@ -152,9 +180,6 @@ line-height: 16px; letter-spacing: 0.25px; } - .mat-mdc-select-value { - color: rgba(0, 0, 0, 0.38); - } .mat-mdc-select-arrow-wrapper { height: 12px; padding-left: 6px; diff --git a/ui-ngx/src/app/shared/components/toggle-header.component.ts b/ui-ngx/src/app/shared/components/toggle-header.component.ts index 35daad0e3f..5781f9fac9 100644 --- a/ui-ngx/src/app/shared/components/toggle-header.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-header.component.ts @@ -15,27 +15,33 @@ /// import { - AfterContentInit, + AfterContentChecked, + AfterContentInit, AfterViewChecked, + AfterViewInit, ChangeDetectorRef, Component, ContentChildren, Directive, ElementRef, EventEmitter, + HostBinding, Input, OnDestroy, OnInit, Output, - QueryList + QueryList, + ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; import { startWith, takeUntil } from 'rxjs/operators'; +import { Platform } from '@angular/cdk/platform'; +import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; export interface ToggleHeaderOption { name: string; @@ -44,6 +50,8 @@ export interface ToggleHeaderOption { export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked'; +export type ScrollDirection = 'after' | 'before'; + @Directive( { // eslint-disable-next-line @angular-eslint/directive-selector @@ -72,7 +80,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI @Input() options: ToggleHeaderOption[] = []; - private _destroyed = new Subject(); + protected _destroyed = new Subject(); protected constructor(protected store: Store) { super(store); @@ -109,7 +117,36 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI templateUrl: './toggle-header.component.html', styleUrls: ['./toggle-header.component.scss'] }) -export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterContentInit, OnDestroy { +export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterViewInit, AfterContentInit, + AfterContentChecked, AfterViewChecked, OnDestroy { + + @ViewChild('toggleGroup', {static: false}) + toggleGroup: ElementRef; + + @ViewChild(MatButtonToggleGroup, {static: false}) + buttonToggleGroup: MatButtonToggleGroup; + + @ViewChild('toggleGroupContainer', {static: false}) + toggleGroupContainer: ElementRef; + + @HostBinding('class.tb-toggle-header-pagination-controls-enabled') + private showPaginationControls = false; + private _showPaginationControlsChanged = false; + + private toggleGroupResize$: ResizeObserver; + + leftPaginationEnabled = false; + rightPaginationEnabled = false; + + private _scrollDistance = 0; + private _scrollDistanceChanged: boolean; + + get scrollDistance(): number { + return this._scrollDistance; + } + set scrollDistance(value: number) { + this._scrollTo(value); + } @Input() value: any; @@ -122,7 +159,22 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC @Input() @coerceBoolean() - useSelectOnMdLg = true; + disablePagination = false; + + @Input() + selectMediaBreakpoint = 'md-lg'; + + @Input() + @coerceBoolean() + set useSelectOnMdLg(value: boolean) { + if (value) { + this.selectMediaBreakpoint = 'md-lg'; + } else { + if (this.selectMediaBreakpoint === 'md-lg') { + this.selectMediaBreakpoint = ''; + } + } + } @Input() @coerceBoolean() @@ -135,28 +187,210 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC @coerceBoolean() disabled = false; - isMdLg: boolean; + get isMdLg(): boolean { + return !this.ignoreMdLgSize && this.isMdLgValue; + } + + private isMdLgValue: boolean; + private useSelectSubject = new BehaviorSubject(false); + + useSelect$ = this.useSelectSubject.asObservable(); private observeBreakpointSubscription: Subscription; constructor(protected store: Store, private cd: ChangeDetectorRef, + private platform: Platform, private breakpointObserver: BreakpointObserver) { super(store); } ngOnInit() { - this.isMdLg = this.breakpointObserver.isMatched(MediaBreakpoints['md-lg']); + const mediaBreakpoints = [MediaBreakpoints['md-lg']]; + if (this.selectMediaBreakpoint && this.selectMediaBreakpoint !== 'md-lg') { + mediaBreakpoints.push(MediaBreakpoints[this.selectMediaBreakpoint]); + } this.observeBreakpointSubscription = this.breakpointObserver - .observe(MediaBreakpoints['md-lg']) + .observe(mediaBreakpoints) .subscribe((state: BreakpointState) => { - this.isMdLg = state.matches; + this.isMdLgValue = state.breakpoints[MediaBreakpoints['md-lg']]; + if (this.selectMediaBreakpoint) { + this.useSelectSubject.next(state.breakpoints[MediaBreakpoints[this.selectMediaBreakpoint]]); + } else { + this.useSelectSubject.next(false); + } this.cd.markForCheck(); } ); + if (!this.disablePagination) { + this.valueChange.pipe(takeUntil(this._destroyed)).subscribe(() => { + this.scrollToToggleOptionValue(); + }); + } + } + + ngOnDestroy() { + this.stopObservePagination(); + super.ngOnDestroy(); + } + + ngAfterViewInit() { + if (!this.disablePagination) { + this.useSelect$.pipe(takeUntil(this._destroyed)).subscribe((useSelect) => { + if (useSelect) { + this.removePagination(); + } else { + setTimeout(() => { + this.startObservePagination(); + }, 0); + } + }); + } + } + + ngAfterContentChecked() { + if (this._scrollDistanceChanged) { + this.updateToggleHeaderScrollPosition(); + this._scrollDistanceChanged = false; + this.cd.markForCheck(); + } + } + + ngAfterViewChecked() { + if (this._showPaginationControlsChanged) { + this.scrollToToggleOptionValue(); + this._showPaginationControlsChanged = false; + this.cd.markForCheck(); + } } trackByHeaderOption(index: number, option: ToggleHeaderOption){ return option.value; } + + handlePaginatorClick(direction: ScrollDirection, $event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.scrollHeader(direction); + } + + handlePaginatorTouchStart(direction: ScrollDirection, $event: Event) { + if (direction === 'before' && !this.leftPaginationEnabled || + direction === 'after' && !this.rightPaginationEnabled) { + $event.preventDefault(); + } + } + + private startObservePagination() { + this.toggleGroupResize$ = new ResizeObserver(() => { + this.updatePagination(); + }); + this.toggleGroupResize$.observe(this.toggleGroupContainer.nativeElement); + } + + private removePagination() { + this.stopObservePagination(); + this.showPaginationControls = false; + } + + private stopObservePagination() { + if (this.toggleGroupResize$) { + this.toggleGroupResize$.disconnect(); + this.toggleGroupResize$ = null; + } + } + + private scrollHeader(direction: ScrollDirection) { + const viewLength = this.toggleGroup.nativeElement.offsetWidth; + // Move the scroll distance one-third the length of the tab list's viewport. + const scrollAmount = ((direction === 'before' ? -1 : 1) * viewLength) / 3; + return this._scrollTo(this._scrollDistance + scrollAmount); + } + + private scrollToToggleOptionValue() { + if (this.buttonToggleGroup && this.buttonToggleGroup.selected) { + const selectedToggleButton = this.buttonToggleGroup.selected as MatButtonToggle; + const index = this.options.findIndex(o => o.value === selectedToggleButton.value); + const isLast = index === this.options.length - 1; + const isFirst = index === 0; + const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth; + const {offsetLeft, offsetWidth} = (selectedToggleButton._buttonElement.nativeElement.offsetParent as HTMLElement); + const labelBeforePos = isFirst ? 0 : offsetLeft; + const labelAfterPos = isLast ? this.toggleGroup.nativeElement.scrollWidth : labelBeforePos + offsetWidth; + const beforeVisiblePos = this.scrollDistance; + const afterVisiblePos = this.scrollDistance + viewLength; + if (labelBeforePos < beforeVisiblePos) { + this.scrollDistance -= beforeVisiblePos - labelBeforePos; + } else if (labelAfterPos > afterVisiblePos) { + this.scrollDistance += Math.min( + labelAfterPos - afterVisiblePos, + labelBeforePos - beforeVisiblePos, + ); + } + } + } + + private updatePagination() { + this.checkPaginationEnabled(); + this.checkPaginationControls(); + this.updateToggleHeaderScrollPosition(); + } + + private checkPaginationEnabled() { + if (this.toggleGroupContainer) { + const isEnabled = this.toggleGroup.nativeElement.scrollWidth > this.toggleGroupContainer.nativeElement.offsetWidth; + if (isEnabled !== this.showPaginationControls) { + if (!isEnabled) { + this.scrollDistance = 0; + } else { + this._showPaginationControlsChanged = true; + } + this.cd.markForCheck(); + this.showPaginationControls = isEnabled; + } + } else { + this.showPaginationControls = false; + } + } + + private checkPaginationControls() { + if (!this.showPaginationControls) { + this.leftPaginationEnabled = this.rightPaginationEnabled = false; + } else { + // Check if the pagination arrows should be activated. + this.leftPaginationEnabled = this.scrollDistance > 0; + this.rightPaginationEnabled = this.scrollDistance < this.getMaxScrollDistance(); + this.cd.markForCheck(); + } + } + + private getMaxScrollDistance(): number { + const lengthOfToggleGroup = this.toggleGroup.nativeElement.scrollWidth; + const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth; + return lengthOfToggleGroup - viewLength || 0; + } + + private _scrollTo(position: number) { + if (!this.showPaginationControls) { + return {maxScrollDistance: 0, distance: 0}; + } else { + const maxScrollDistance = this.getMaxScrollDistance(); + this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position)); + this._scrollDistanceChanged = true; + this.checkPaginationControls(); + return {maxScrollDistance, distance: this._scrollDistance}; + } + } + + private updateToggleHeaderScrollPosition() { + if (this.toggleGroupContainer) { + const scrollDistance = this.scrollDistance; + const translateX = -scrollDistance; + this.toggleGroup.nativeElement.style.transform = `translateX(${Math.round(translateX)}px)`; + if (this.platform.TRIDENT || this.platform.EDGE) { + this.toggleGroupContainer.nativeElement.scrollLeft = 0; + } + } + } } diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.html b/ui-ngx/src/app/shared/components/toggle-select.component.html index a5ce7778b5..c03e0cfcbf 100644 --- a/ui-ngx/src/app/shared/components/toggle-select.component.html +++ b/ui-ngx/src/app/shared/components/toggle-select.component.html @@ -20,6 +20,8 @@ useSelectOnMdLg="false" [disabled]="disabled" [appearance]="appearance" + [disablePagination]="disablePagination" + [selectMediaBreakpoint]="selectMediaBreakpoint" [options]="options" [value]="modelValue" (valueChange)="updateModel($event)"> diff --git a/ui-ngx/src/app/shared/components/toggle-select.component.ts b/ui-ngx/src/app/shared/components/toggle-select.component.ts index 8338e04f6a..c1541d1128 100644 --- a/ui-ngx/src/app/shared/components/toggle-select.component.ts +++ b/ui-ngx/src/app/shared/components/toggle-select.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input } from '@angular/core'; +import { Component, forwardRef, HostBinding, Input } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -35,13 +35,23 @@ import { coerceBoolean } from '@shared/decorators/coercion'; }) export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor { + @HostBinding('style.maxWidth') + get maxWidth() { return '100%'; } + @Input() @coerceBoolean() disabled: boolean; + @Input() + selectMediaBreakpoint; + @Input() appearance: ToggleHeaderAppearance = 'stroked'; + @Input() + @coerceBoolean() + disablePagination = false; + modelValue: any; private propagateChange = null; diff --git a/ui-ngx/src/app/shared/components/unit-input.component.html b/ui-ngx/src/app/shared/components/unit-input.component.html index d001a43ef2..c15b232444 100644 --- a/ui-ngx/src/app/shared/components/unit-input.component.html +++ b/ui-ngx/src/app/shared/components/unit-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + > { if (this.fetchUnits$ === null) { - this.fetchUnits$ = this.resourcesService.loadJsonResource>(unitsModels).pipe( + this.fetchUnits$ = getUnits(this.resourcesService).pipe( map(units => units.map(u => ({ symbol: u.symbol, name: this.translate.instant(u.name), diff --git a/ui-ngx/src/app/shared/components/user-menu.component.html b/ui-ngx/src/app/shared/components/user-menu.component.html index a25a0fec25..afb6e66437 100644 --- a/ui-ngx/src/app/shared/components/user-menu.component.html +++ b/ui-ngx/src/app/shared/components/user-menu.component.html @@ -28,13 +28,9 @@
- -