diff --git a/.gitattributes b/.gitattributes index 2d407a27d836e314a93305bcefd826e41d051904..ea9df6aed0c1507dc691b9d30174895c1bba1bab 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1769,6 +1769,7 @@ LCS/WinCCWrapper/include/CMakeLists.txt -text LCS/WinCCWrapper/include/WinCCManager.h -text LCS/WinCCWrapper/include/WinCCResources.h -text LCS/WinCCWrapper/include/WinCCWrapper.h -text +LCS/WinCCWrapper/include/exceptions.h -text LCS/WinCCWrapper/src/CMakeLists.txt -text LCS/WinCCWrapper/src/ConnectWaitForAnswer.cc -text LCS/WinCCWrapper/src/ConnectWaitForAnswer.h -text @@ -1777,8 +1778,10 @@ LCS/WinCCWrapper/src/WinCCResources.cc -text LCS/WinCCWrapper/src/WinCCWrapper.cc -text LCS/WinCCWrapper/src/WinCCWrapper_boost_python.cc -text LCS/WinCCWrapper/src/__init__.py -text +LCS/WinCCWrapper/src/exceptions.h -text LCS/WinCCWrapper/test/CMakeLists.txt -text LCS/WinCCWrapper/test/WinCCGet.cc -text +LCS/WinCCWrapper/test/WinCCQuery.cc -text LCS/WinCCWrapper/test/WinCCSet.cc -text LCS/WinCCWrapper/test/mock.py -text LCS/doc/package.dox -text @@ -1835,8 +1838,6 @@ LCU/Maintenance/DBInterface/monitoringdb/__init__.py -text LCU/Maintenance/DBInterface/monitoringdb/admin.py -text LCU/Maintenance/DBInterface/monitoringdb/apps.py -text LCU/Maintenance/DBInterface/monitoringdb/exceptions.py -text -LCU/Maintenance/DBInterface/monitoringdb/migrations/0001_initial.py -text -LCU/Maintenance/DBInterface/monitoringdb/migrations/0002_winccantennastatuses.py -text LCU/Maintenance/DBInterface/monitoringdb/migrations/__init__.py -text LCU/Maintenance/DBInterface/monitoringdb/models/__init__.py -text LCU/Maintenance/DBInterface/monitoringdb/models/component.py -text @@ -1846,8 +1847,10 @@ LCU/Maintenance/DBInterface/monitoringdb/models/element_error.py -text LCU/Maintenance/DBInterface/monitoringdb/models/fixed_types.py -text LCU/Maintenance/DBInterface/monitoringdb/models/log.py -text LCU/Maintenance/DBInterface/monitoringdb/models/rtsm.py -text +LCU/Maintenance/DBInterface/monitoringdb/models/signals/generic_test_insert.py -text LCU/Maintenance/DBInterface/monitoringdb/models/station.py -text LCU/Maintenance/DBInterface/monitoringdb/models/station_test.py -text +LCU/Maintenance/DBInterface/monitoringdb/models/test.py -text LCU/Maintenance/DBInterface/monitoringdb/models/wincc.py -text LCU/Maintenance/DBInterface/monitoringdb/pagination.py -text LCU/Maintenance/DBInterface/monitoringdb/rtsm_test_raw_parser.py -text @@ -4234,6 +4237,28 @@ MAC/Tools/jtagctl/rsp_infraCnn.Bin -text svneol=unset#unset MAC/Tools/jtagctl/rsp_interApl.Bin -text svneol=unset#unset MAC/Tools/jtagctl/rsp_interCnn.Bin -text svneol=unset#unset MAC/Tools/jtagctl/testdata.bin -text svneol=unset#unset +MAC/WinCCDBBridge/CMakeLists.txt -text +MAC/WinCCDBBridge/DB.ini -text +MAC/WinCCDBBridge/Docker/Dockerfile -text +MAC/WinCCDBBridge/Docker/LOFAR.tar.gz -text svneol=unset#application/x-gzip +MAC/WinCCDBBridge/Docker/run.sh -text +MAC/WinCCDBBridge/etc/CMakeLists.txt -text +MAC/WinCCDBBridge/etc/DB.ini -text +MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xml -text +MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xsd -text +MAC/WinCCDBBridge/src/CMakeLists.txt -text +MAC/WinCCDBBridge/src/DatapointAttribute.cpp -text +MAC/WinCCDBBridge/src/DatapointAttribute.hpp -text +MAC/WinCCDBBridge/src/DatapointIndex.cpp -text +MAC/WinCCDBBridge/src/DatapointIndex.hpp -text +MAC/WinCCDBBridge/src/DatapointRule.cpp -text +MAC/WinCCDBBridge/src/DatapointRule.hpp -text +MAC/WinCCDBBridge/src/Program.cpp -text +MAC/WinCCDBBridge/src/Program.hpp -text +MAC/WinCCDBBridge/src/main.cpp -text +MAC/WinCCDBBridge/test/CMakeLists.txt -text +MAC/WinCCDBBridge/test/t_DatapointAttribute.cpp -text +MAC/WinCCDBBridge/test/t_DatapointIndex.cpp -text MAC/WinCCPublisher/CMakeLists.txt -text MAC/WinCCPublisher/config/WinCCPublisher.conf -text MAC/WinCCPublisher/doc/package.dox -text diff --git a/CMake/LofarPackageList.cmake b/CMake/LofarPackageList.cmake index 242feb2178656fb7cb7544af75ed3286fafa0e0f..59dbec37fc4c3dd1c74b516bc9c8f6ca3e19d17a 100644 --- a/CMake/LofarPackageList.cmake +++ b/CMake/LofarPackageList.cmake @@ -1,7 +1,7 @@ # - Create for each LOFAR package a variable containing the absolute path to # its source directory. # -# Generated by gen_LofarPackageList_cmake.sh at Tue Feb 26 11:44:22 CET 2019 +# Generated by gen_LofarPackageList_cmake.sh at Wed May 29 15:45:16 CEST 2019 # # ---- DO NOT EDIT ---- # @@ -148,6 +148,8 @@ if(NOT DEFINED LOFAR_PACKAGE_LIST_INCLUDED) set(OTDB_Comps_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/Deployment/data/OTDB) set(StaticMetaData_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/Deployment/data/StaticMetaData) set(WinCCPublisher_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/WinCCPublisher) + set(WinCCREST_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/WinCCREST) + set(WinCCDBBridge_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/WinCCDBBridge) set(TaskManagement_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/Services/TaskManagement) set(TBB_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/TBB) set(GCFTM_SOURCE_DIR ${CMAKE_SOURCE_DIR}/MAC/GCF/TM) diff --git a/LCS/WinCCWrapper/include/CMakeLists.txt b/LCS/WinCCWrapper/include/CMakeLists.txt index 9341a3420be5b55ea0c821bc02652445231d2430..48366da002dc0da6ca064036ffaf96e0f46532a4 100644 --- a/LCS/WinCCWrapper/include/CMakeLists.txt +++ b/LCS/WinCCWrapper/include/CMakeLists.txt @@ -1,5 +1,5 @@ # Create symbolic link to include directory. -execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink +execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/include/${PACKAGE_NAME}) @@ -8,4 +8,5 @@ install(FILES WinCCManager.h WinCCResources.h WinCCWrapper.h + exceptions.h DESTINATION include/${PACKAGE_NAME}) diff --git a/LCS/WinCCWrapper/include/WinCCManager.h b/LCS/WinCCWrapper/include/WinCCManager.h index 69e70cbb947cf731c2a3bbf24224aa4ed7c16b5c..c29d37cefaad6c4d20936064db3ae4c074cf68b4 100644 --- a/LCS/WinCCWrapper/include/WinCCManager.h +++ b/LCS/WinCCWrapper/include/WinCCManager.h @@ -31,6 +31,7 @@ #include <Variable.hxx> #include <boost/python.hpp> +#include <boost/any.hpp> namespace LOFAR { namespace WINCCWRAPPER { @@ -41,7 +42,6 @@ class WinCCManager : public Manager public: //! default constructor WinCCManager(); - //! run the manager, connect to the running wincc instance. void run(); //! exit the manager, disconnect from the running wincc instance. @@ -49,8 +49,15 @@ public: //! subscribe for changes on this list of datapoints. Whenever any of these datapoints changes value, the callback function from set_connect_datapoints_callback is called with the changed datapoint name and value. void connect_datapoints(const std::vector<std::string> &data_points); + //! subscribe for changes on this list of datapoints. Whenever any of these datapoints change all the values from these datapoints gets returned in one call. + void connect_datapoints_multiple(const std::vector<std::string> &data_points); + //! unsubscribe for changes on this list of datapoints. + void disconnect_datapoints(const std::vector<std::string> &data_points); + //! provide your callback function which is called whenever any of the connected datapoints is changed. + void set_connect_datapoints_callback(std::function<void(std::string name, std::string value)> callback_functor) {callback = callback_functor;}; //! provide your callback function which is called whenever any of the connected datapoints is changed. - void set_connect_datapoints_callback(std::function<void(std::string name, std::string value)> callback) {callback = callback;}; + void set_connect_datapoints_callback(std::function<void(std::map<std::string, std::string>)> callback_functor) {callback_multi = callback_functor;}; + //! set the datapoint with given name to the given value, returns true upon success. /*! set the datapoint with given name to the given value @@ -75,7 +82,10 @@ public: bool get_datapoint(const std::string &name, boost::python::list &value); //! get the datapoint with the given name and return it's std::vector<int> value in parameter value. returns true upon success. bool get_datapoint(const std::string &name, std::vector<int> &value); - + //! get the datapoint with a any type + bool get_datapoint_any(const std::string &datapoint_name, boost::any &value); + //! set the datapoint with a any type + bool set_datapoint_any(const std::string &datapoint_name, const boost::any &value); //! mark the datapoint with given name valid. returns true upon success. bool set_datapoint_valid(const std::string &name) { return set_datapoint_validity(name, true, nullptr); } //! mark the datapoint with given name invalid. returns true upon success. @@ -83,24 +93,32 @@ public: //! set the datapoint with given name valid/invalid. returns true upon success. bool set_datapoint_validity(const std::string &name, bool validity, const Variable *value=nullptr); + bool get_query(const std::string &query, std::vector<std::vector<std::string>> & result); //! handle signals to exit nicely virtual void signalHandler(int sig); + + void wait_for_event(long sec, long microSec); + private: void init(); friend class ConnectWaitForAnswer; void handle_hotlink(const std::string &name, const std::string &value); + void handle_hotlink(const std::map<std::string, std::string> &); + void handle_get(const std::string &name, Variable *&value); bool request_datapoint(const std::string &name); + bool request_query_result(const std::string &query, PVSSulong & identifier); template <typename Tval> bool _get_datapoint(const std::string &name, Tval &value); template <typename Tval> bool _get_datapoint(const std::string &name, Tval *value); + bool get_datapoint_variable(const std::string &name, Variable *&value); bool has_received_variable(const std::string &name); @@ -109,6 +127,7 @@ private: volatile static bool doExit; std::function<void(std::string name, std::string value)> callback; + std::function<void(std::map<std::string, std::string>)> callback_multi; std::mutex mtx; std::condition_variable cv; std::map<std::string, Variable *> values; diff --git a/LCS/WinCCWrapper/include/WinCCResources.h b/LCS/WinCCWrapper/include/WinCCResources.h index 552e9fcf771938a27fdad189e915440db5f91500..b387368eb42f7c379894621960b07de3db0ea905 100644 --- a/LCS/WinCCWrapper/include/WinCCResources.h +++ b/LCS/WinCCWrapper/include/WinCCResources.h @@ -27,9 +27,12 @@ namespace LOFAR { class WinCCResources { public: - WinCCResources(const std::string &projec_name); + WinCCResources(const std::string &project_name); + + WinCCResources(const std::string &program_name, const std::string &project_name, const int num); private: - void init(const std::string &projec_name); + void init(const std::string &project_name); + void init(const std::string &program_name, const std::string &project_name, const int num); }; } // namespace WINCCWRAPPER diff --git a/LCS/WinCCWrapper/include/WinCCWrapper.h b/LCS/WinCCWrapper/include/WinCCWrapper.h index 26fa060d6064ee4fcff3f620e44b86a90c52327a..4e57aa17166f24a9fc34c283d9dce20047f61ee1 100644 --- a/LCS/WinCCWrapper/include/WinCCWrapper.h +++ b/LCS/WinCCWrapper/include/WinCCWrapper.h @@ -21,6 +21,7 @@ #define WINCCSERVICES_WINCC_WRAPPER_H #include <string> +#include <boost/variant.hpp> #include <vector> #include <ctime> #include <functional> @@ -29,6 +30,7 @@ #include "WinCCManager.h" #include <boost/python.hpp> +#include <boost/any.hpp> namespace LOFAR { namespace WINCCWRAPPER { @@ -40,6 +42,9 @@ class WinCCWrapper public: //! default constructor WinCCWrapper(const std::string &project_name); + + //! default constructor + WinCCWrapper(const std::string &program_name, const std::string &project_name, const int num); //! disconnect from the running wincc instance. void exit(); //! connect to the running wincc instance. @@ -47,14 +52,21 @@ public: //! subscribe for changes on this list of datapoints. Whenever any of these datapoints changes value, the callback function from set_connect_datapoints_callback is called with the changed datapoint name and value. void connect_datapoints(const std::vector<std::string> &data_points); + //! subscribe for changes on this list of datapoints. Whenever any of these datapoints changes value, the callback function from set_connect_datapoints_callback is called with all the specified values. + void connect_datapoints_multi(const std::vector<std::string> &data_points); + + //! unsubscribe for changes on the given list of datapoints. + void disconnect_datapoints(const std::vector<std::string> &data_points); //! provide your callback function which is called whenever any of the connected datapoints is changed. void set_connect_datapoints_callback(std::function<void(std::string name, std::string value)> callback); + //! provide your callback function which is called whenever any of the connected datapoints is changed. It returns all the specified datapoints even if only one changes. + void set_connect_datapoints_callback(std::function<void(std::map<std::string, std::string>)> callback); //! set the datapoint with given name to the given int value, mark it valid/invalid, returns true upon success. bool set_datapoint(const std::string &name, int value, bool valid=true); //! set the datapoint with given name to the given boost::python::list value, mark it valid/invalid, returns true upon success. bool set_datapoint(const std::string &name, boost::python::list &value, bool valid=true); - //! set the datapoint with given name to the given boost::python::tuple value, mark it valid/invalid, returns true upon success. + //! set the datapoint with given name to the given boost::python::tuple value, mark it valid/invalid, returns true upon success. bool set_datapoint(const std::string &name, boost::python::tuple &value, bool valid=true); //! set the datapoint with given name to the given std::vector<int> value, mark it valid/invalid, returns true upon success. bool set_datapoint(const std::string &name, std::vector<int> &value, bool valid=true); @@ -70,11 +82,14 @@ public: //! set the datapoint with given name to the given time_t value, mark it valid/invalid, returns true upon success. bool set_datapoint_time(const std::string &name, time_t value, bool valid=true); + bool set_datapoint_any(const std::string &name, boost::any value); //! mark the datapoint with given name valid. bool set_datapoint_valid(const std::string &name); //! mark the datapoint with given name invalid. bool set_datapoint_invalid(const std::string &name); + //! provide the boost::any for the given datapoint name + boost::any get_datapoint_any (const std::string &name); //! get the datapoint with the given name and return it as an int value if possible, otherwise an exception is raised. int get_datapoint_int(const std::string &name); //! get the datapoint with the given name and return it as a long value if possible, otherwise an exception is raised. @@ -88,11 +103,17 @@ public: //! get the datapoint with the given name and return it as a time_t value if possible, otherwise an exception is raised. time_t get_datapoint_time(const std::string &name); //! get the datapoint with the given name and return it as a boost::python::list value if possible, otherwise an exception is raised. - boost::python::list get_datapoint_list(const std::string &name); + boost::python::list get_datapoint_list(const std::string &name); + //! get the datapoint last set time + time_t get_datapoint_set_time(const std::string &name); //! get the datapoint with the given name and return it as a std::vector<int> value if possible, otherwise an exception is raised. //! this method is used in the WinCCGet test std::vector<int> get_datapoint_vector(const std::string &name); + //! get the datapoint with the given name and return it as a std::vector<std::string> value. + std::vector<std::string> get_datapoint_vector_string(const std::string &name); + std::string get_formatted_datapoint(const std::string &name); + void wait_for_event(long sec, long microSec); private: // get_datapoint template <typename T> diff --git a/LCS/WinCCWrapper/include/exceptions.h b/LCS/WinCCWrapper/include/exceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..998ab30809e845243ed043bdc8159bbb7cd7a5bd --- /dev/null +++ b/LCS/WinCCWrapper/include/exceptions.h @@ -0,0 +1,23 @@ +#ifndef WINCC_EXCEPTIONS +#define WINCC_EXCEPTIONS + +namespace LOFAR{ +namespace WINCCWRAPPER{ + +class DatapointNameNotFound : public std::exception +{ +private: + std::string message; +public: + DatapointNameNotFound(const std::string & datapointName): + message{"Datapoint " + datapointName + " not found"}{} + const char * what () const throw () + { + return message.c_str(); + } +}; + +} +} + +#endif diff --git a/LCS/WinCCWrapper/src/ConnectWaitForAnswer.cc b/LCS/WinCCWrapper/src/ConnectWaitForAnswer.cc index 767ea576d761ca1ee4e99202ba2f07af82c3ba11..b35c3cfe0396b49532f141ca1161d76e17e9a9aa 100644 --- a/LCS/WinCCWrapper/src/ConnectWaitForAnswer.cc +++ b/LCS/WinCCWrapper/src/ConnectWaitForAnswer.cc @@ -25,13 +25,22 @@ namespace LOFAR { using namespace std; +ConnectWaitForAnswer::ConnectWaitForAnswer(const bool handle_multi): HotLinkWaitForAnswer{}, multi{handle_multi}{ + +} + +ConnectWaitForAnswer::ConnectWaitForAnswer(): HotLinkWaitForAnswer{}{ + +} + void ConnectWaitForAnswer::hotLinkCallBack(DpMsgAnswer &answer) { + const std::string answer_id{std::to_string(answer.getAnswerId())}; for (AnswerGroup *grpPtr = answer.getFirstGroup(); grpPtr; grpPtr = answer.getNextGroup()) { if (grpPtr->getError()) { - cout << "Error!" << endl; + cout <<grpPtr->getError()->toString()<< endl; } else { @@ -42,6 +51,8 @@ void ConnectWaitForAnswer::hotLinkCallBack(DpMsgAnswer &answer) if(varPtr) { string name = get_datapoint_name(item); + if(name.compare("") == 0) name += answer_id; + Variable *value = varPtr->clone(); //WinCCManager should delete cloned pointer ((WinCCManager *) Manager::getManPtr())->handle_get(name, value); @@ -52,6 +63,14 @@ void ConnectWaitForAnswer::hotLinkCallBack(DpMsgAnswer &answer) } void ConnectWaitForAnswer::hotLinkCallBack(DpHLGroup &group) +{ + if(multi){ + handle_multi(group); + }else{ + handle_one_by_one(group); + } +} +void ConnectWaitForAnswer::handle_one_by_one(DpHLGroup &group) { for (DpVCItem *item = group.getFirstItem(); item; item = group.getNextItem()) { @@ -59,6 +78,25 @@ void ConnectWaitForAnswer::hotLinkCallBack(DpHLGroup &group) } } +void ConnectWaitForAnswer::handle_multi(DpHLGroup &group) +{ + + std::map<std::string, std::string> values; + for (DpVCItem *item = group.getFirstItem(); item; item = group.getNextItem()) + { + Variable *varPtr = item->getValuePtr(); + + if (varPtr){ + const string name = get_datapoint_name(item); + const string value = varPtr->formatValue().c_str(); + + values[name] = value; + } + } + ((WinCCManager *) Manager::getManPtr())->handle_hotlink(values); +} + + void ConnectWaitForAnswer::handle_group_item(const DpVCItem* const item) { Variable *varPtr = item->getValuePtr(); @@ -66,6 +104,7 @@ void ConnectWaitForAnswer::handle_group_item(const DpVCItem* const item) if (varPtr) { string name = get_datapoint_name(item); + string value = varPtr->formatValue().c_str(); ((WinCCManager *) Manager::getManPtr())->handle_hotlink(name, value); diff --git a/LCS/WinCCWrapper/src/ConnectWaitForAnswer.h b/LCS/WinCCWrapper/src/ConnectWaitForAnswer.h index 28dd3f0138a5c84e25374ade4ee2bea51ad9745a..dba485153cad7e3848ec381da58588b903639bb6 100644 --- a/LCS/WinCCWrapper/src/ConnectWaitForAnswer.h +++ b/LCS/WinCCWrapper/src/ConnectWaitForAnswer.h @@ -32,12 +32,18 @@ namespace LOFAR { class ConnectWaitForAnswer : public HotLinkWaitForAnswer { public: + ConnectWaitForAnswer(); + ConnectWaitForAnswer(const bool handle_multi); using HotLinkWaitForAnswer::hotLinkCallBack; virtual void hotLinkCallBack(DpMsgAnswer &answer); virtual void hotLinkCallBack(DpHLGroup &group); private: + bool multi; + void handle_one_by_one(DpHLGroup &group); + void handle_multi(DpHLGroup &group); void handle_group_item(const DpVCItem* const item); std::string get_datapoint_name(const DpVCItem* const item); + }; } // namespace WINCCWRAPPER diff --git a/LCS/WinCCWrapper/src/WinCCManager.cc b/LCS/WinCCWrapper/src/WinCCManager.cc index 6aa6c34b42ef58025ee2c2e37199c6820783e7f0..68559dc23a634ba85da0bd413e07c482128afaf5 100644 --- a/LCS/WinCCWrapper/src/WinCCManager.cc +++ b/LCS/WinCCWrapper/src/WinCCManager.cc @@ -34,9 +34,12 @@ #include <TextVar.hxx> #include <DynVar.hxx> #include <DynPtrArray.hxx> +#include <Resources.hxx> #include <cassert> #include <vector> #include <boost/python.hpp> +#include <boost/any.hpp> +#include <exceptions.h> namespace LOFAR { namespace WINCCWRAPPER { @@ -100,12 +103,77 @@ void WinCCManager::connect_datapoints(const std::vector<std::string> &dataPoints } } +void WinCCManager::connect_datapoints_multiple(const std::vector<std::string> &dataPoints) +{ + + DpIdentList dpList; + for(vector<string>::const_iterator it = dataPoints.begin(); it != dataPoints.end(); it++) + { + DpIdentifier dpIdConnect; + + if (Manager::getId(it->c_str(), dpIdConnect) == PVSS_FALSE) + { + // This name was unknown + ErrHdl::error(ErrClass::PRIO_SEVERE, + ErrClass::ERR_PARAM, + ErrClass::UNEXPECTEDSTATE, + "PublishManager", + "connect_datapoints", + CharString("Datapoint ") + CharString(it->c_str()) + + CharString(" missing")); + } + else + { + dpList.append(dpIdConnect); + + } + } + // We give the dpConnect a nice naked pointer because it will delete it when the manager stops. + HotLinkWaitForAnswer* wait = new ConnectWaitForAnswer(true); + Manager::dpConnect(dpList, wait); +} + + +void WinCCManager::disconnect_datapoints(const std::vector<std::string> &dataPoints) +{ + for(vector<string>::const_iterator it = dataPoints.begin(); it != dataPoints.end(); it++) + { + DpIdentifier dpIdConnect; + + if (Manager::getId(it->c_str(), dpIdConnect) == PVSS_FALSE) + { + // This name was unknown + ErrHdl::error(ErrClass::PRIO_SEVERE, + ErrClass::ERR_PARAM, + ErrClass::UNEXPECTEDSTATE, + "PublishManager", + "connect_datapoints", + CharString("Datapoint ") + CharString(it->c_str()) + + CharString(" missing")); + } + else + { + // We give the dpConnect a nice naked pointer because it will delete it when the manager stops. + HotLinkWaitForAnswer* wait = new ConnectWaitForAnswer(); + Manager::dpDisconnect(dpIdConnect, wait); + } + } +} + bool WinCCManager::set_datapoint(const std::string &name, const Variable &value, bool valid) { //reuse the set_datapoint_validity, and explicitly set the value to the given value return set_datapoint_validity(name, valid, &value); } + +// request the query (async). is called by _get_query which makes it blocking (synchronous) by waiting for the answer. +bool WinCCManager::request_query_result(const std::string &query, PVSSulong & identifier) +{ + HotLinkWaitForAnswer* wait = new ConnectWaitForAnswer(); + return (PVSS_TRUE == Manager::dpQuery(query.c_str(), identifier, wait)); +} + // request the datapoint (async). is called by _get_datapoint which makes it blocking (synchronous) by waiting for the answer. bool WinCCManager::request_datapoint(const std::string &name) { @@ -333,6 +401,72 @@ Variable::ConvertResult convert(Variable *var, struct tm &value, Variable *&conv return cr; } +boost::any convert_any(Variable * variable, boost::any & value){ + Variable * converted_variable{nullptr}; + switch(variable->isA()){ + case VariableType::INTEGER_VAR:{ + int casted_variable; + convert(variable, casted_variable, converted_variable); + value = casted_variable; + + break; + } + case VariableType::FLOAT_VAR: { + float casted_variable; + convert(variable, casted_variable, converted_variable); + value = casted_variable; + + break; + } + case VariableType::TEXT_VAR: { + std::string casted_variable; + convert(variable, casted_variable, converted_variable); + value = casted_variable; + + break; + } + case VariableType::LONG_VAR:{ + // Find the wincc guy who though this out + long casted_variable; + convert(variable, casted_variable, converted_variable); + value = casted_variable; + + break; + } + case VariableType::BIT_VAR: { + bool casted_variable; + convert(variable, casted_variable, converted_variable); + value = casted_variable; + + break; + } + case VariableType::TIME_VAR: { + time_t casted_variable; + convert(variable, casted_variable, converted_variable); + value = casted_variable; + + break; + } + case VariableType::ANYTYPE_VAR: { + const AnyTypeVar * converted_var{static_cast<AnyTypeVar*>(variable)}; + convert_any(converted_var->getVar(), value); + break; + } + case VariableType::DPIDENTIFIER_VAR: { + value = std::string{variable->formatValue().c_str()}; + break; + } + default: + CharString value_str = variable->formatValue(); + std::cerr<<value_str; + std::cerr<<"Datapoint type still not supported: "<<std::hex<<variable->isA()<<std::endl; + value = std::string{value_str.c_str()}; + } + + if(converted_variable != nullptr) delete converted_variable; + return true; +} + //internal generic method to get the typed (Tval) value of a datapoint //used by the public strictly typed methods template <typename Tval> @@ -355,6 +489,42 @@ bool WinCCManager::_get_datapoint(const std::string &name, Tval &value) return false; } +bool convert_to_vector2(Variable * variable, std::vector<std::vector<std::string>> & result){ + if(variable->isA(VariableType::DYN_VAR)){ + const DynVar * rows{static_cast<DynVar *>(variable)}; + for(DynPtrArrayIndex i=0; i < rows->getNumberOfItems(); i++){ + const DynVar * row{static_cast<DynVar *>(rows->getAt(i))}; + std::vector<std::string> * current_row{new std::vector<std::string>{}}; + Variable * column{static_cast<DynVar *>(row->getNext())}; + while(column){ + current_row->push_back(column->formatValue().c_str()); + column = static_cast<DynVar *>(row->getNext()); + } + result.push_back(*current_row); + row = static_cast<DynVar *>(rows->getNext()); + } + return true; + } + return false; +} + +//internal generic method to query a set of WinCC data points. Since the type is not previously know +bool WinCCManager::get_query(const std::string &query, std::vector<std::vector<std::string>> & result){ + Variable * variable_value = nullptr; + PVSSulong identifier; + if(request_query_result(query, identifier)) + { + std::string identifier_str{std::to_string(identifier)}; + if(wait_for_received_variable(identifier_str, 1000)) { + if(get_received_variable(identifier_str, variable_value)) { + convert_to_vector2(variable_value, result); + return true; + } + } + } + + return false; +} //below, a few strictly type methods for get_datapoint are defined //they just call the templated _get_datapoint, so why not just use the one and only templated method? @@ -401,10 +571,11 @@ bool WinCCManager::get_datapoint(const std::string &name, std::vector<int> &valu return _get_datapoint(name, value); } + bool WinCCManager::set_datapoint_validity(const std::string &name, bool validity, const Variable *value) { DpIdentifier dpId; - + if (Manager::getId(name.c_str(), dpId) == PVSS_FALSE) { // This name was unknown. @@ -472,6 +643,10 @@ void WinCCManager::run() } } +void WinCCManager::wait_for_event(long sec, long microSec){ + dispatch(sec, microSec); +} + void WinCCManager::exit() { Manager::exit(0); @@ -485,6 +660,14 @@ void WinCCManager::handle_hotlink(const std::string &name, const std::string &va } } +void WinCCManager::handle_hotlink(const std::map<std::string, std::string> & values) +{ + if(callback_multi) + { + callback_multi(values); + } +} + void WinCCManager::signalHandler(int sig) { if ((sig == SIGINT) || (sig == SIGTERM)) @@ -497,6 +680,81 @@ void WinCCManager::signalHandler(int sig) } } +bool WinCCManager::set_datapoint_any(const std::string &datapoint_name, const boost::any &value){ + Variable * variable{nullptr}; + + if(get_datapoint_variable(datapoint_name, variable)){ + switch(variable->isA()){ + case VariableType::INTEGER_VAR: { + IntegerVar converted_value{boost::any_cast<int>(value)}; + set_datapoint(datapoint_name, converted_value); + break; + } + case VariableType::FLOAT_VAR: { + FloatVar converted_value{boost::any_cast<float>(value)}; + set_datapoint(datapoint_name, converted_value); + break; + } + case VariableType::TEXT_VAR: { + TextVar converted_value{CharString(boost::any_cast<std::string>(value).c_str())}; + set_datapoint(datapoint_name, converted_value); + break; + } + case VariableType::LONG_VAR:{ + LongVar converted_value{boost::any_cast<long>(value)}; + set_datapoint(datapoint_name, converted_value); + + break; + } + case VariableType::BIT_VAR: { + BitVar converted_value{boost::any_cast<bool>(value)}; + set_datapoint(datapoint_name, converted_value); + + break; + } + case VariableType::TIME_VAR: { + TimeVar converted_value(0,0); + converted_value.setSeconds(boost::any_cast<time_t>(value)); + set_datapoint(datapoint_name, converted_value); + + break; + } + default: + std::cerr<<"Datapoint type still not supported: "<<std::hex<<variable->isA()<<std::endl; + return false; + } + }else{ + return false; + } + + return true; +} + +bool WinCCManager::get_datapoint_any(const std::string &datapoint_name, boost::any & value){ + // Yeah I know... WinCC API is getting a &* to allocate the memory space... + Variable * variable{nullptr}; + bool returnValue{true}; + + if(!get_datapoint_variable(datapoint_name, variable)){ + // This name was unknown. + ErrHdl::error(ErrClass::PRIO_SEVERE, + ErrClass::ERR_PARAM, + ErrClass::UNEXPECTEDSTATE, + Resources::getProgName(), + "get_datapoint", + CharString("Datapoint ") + CharString(datapoint_name.c_str()) + + CharString(" missing")); + throw DatapointNameNotFound{datapoint_name}; + } + + convert_any(variable, value); + + if(variable != nullptr) delete variable; + + return returnValue; + +} + } // namespace WINCCWRAPPER } // namespace LOFAR diff --git a/LCS/WinCCWrapper/src/WinCCResources.cc b/LCS/WinCCWrapper/src/WinCCResources.cc index 5eb7dbf8d1e3935abb068f6aa843514ba7b1c84a..a35accb448a859744f62a74adeeb8755ad2bc5f7 100644 --- a/LCS/WinCCWrapper/src/WinCCResources.cc +++ b/LCS/WinCCWrapper/src/WinCCResources.cc @@ -31,11 +31,62 @@ WinCCResources::WinCCResources(const std::string &project_name) init(project_name); } -void WinCCResources::init(const std::string & /*project_name*/) +WinCCResources::WinCCResources(const::std::string &program_name, const std::string &project_name, const int num) { - // TODO set specific project instead of current project. - char* ownArgv[] = {(char *)"WinCCWrapper", (char *)"-currentproj", (char *)"-log", (char *)"+stderr"}; - int ownArgc = sizeof ownArgv / sizeof ownArgv[0]; + init(program_name, project_name, num); +} + + +void WinCCResources::init(const::std::string &program_name, const std::string & project_name, const int num) +{ + std::vector<std::string> args{program_name}; + + if(project_name.compare("-currentproj") == 0){ + args.push_back("-currentproj"); + }else{ + args.push_back("-proj"); + args.push_back(project_name); + } + args.push_back("-log"); + args.push_back("+stderr"); + + args.push_back("-num"); + args.push_back(std::to_string(num)); + + + int ownArgc = args.size(); + + char * ownArgv[ownArgc]; + + for(int i=0; i<ownArgc; i++){ + ownArgv[i] = const_cast<char *>(args.at(i).c_str()); + } + + Resources::init(ownArgc, ownArgv); +} + + + +void WinCCResources::init(const std::string & project_name) +{ + std::vector<std::string> args{"WinCCWrapper"}; + + if(project_name.compare("-currentproj") == 0){ + args.push_back("-currentproj"); + }else{ + args.push_back("-proj"); + args.push_back(project_name); + } + args.push_back("-log"); + args.push_back("+stderr"); + + int ownArgc = args.size(); + + char * ownArgv[ownArgc]; + + for(int i=0; i<ownArgc; i++){ + ownArgv[i] = const_cast<char *>(args.at(i).c_str()); + } Resources::init(ownArgc, ownArgv); } diff --git a/LCS/WinCCWrapper/src/WinCCWrapper.cc b/LCS/WinCCWrapper/src/WinCCWrapper.cc index ab6191dc8d62d0f793b0c91541d59e3e2ecd4986..d8e129f59b8b81ac596a8ae80b6e7c423bd58c0f 100644 --- a/LCS/WinCCWrapper/src/WinCCWrapper.cc +++ b/LCS/WinCCWrapper/src/WinCCWrapper.cc @@ -43,6 +43,9 @@ using namespace std; //! Each datapoint has a human readable name in the wincc database, but the actual value is stored in a sub-item. Append that to each set/get datapoint name. static const string DP_SUFFIX = ":_original.._value"; +//! This property refers to the last set time +static const string DP_SUFFIX_STIME = ":_original.._stime"; + WinCCWrapper::WinCCWrapper(const std::string &project_name) : resources(project_name) @@ -50,6 +53,12 @@ WinCCWrapper::WinCCWrapper(const std::string &project_name) : manager = new WinCCManager(); } +WinCCWrapper::WinCCWrapper(const std::string &program_name, const std::string &project_name, const int num) : + resources(program_name, project_name, num) +{ + manager = new WinCCManager(); +} + void WinCCWrapper::run() { manager->run(); @@ -65,11 +74,31 @@ void WinCCWrapper::connect_datapoints(const std::vector<std::string> &data_point manager->connect_datapoints(data_points); } +void WinCCWrapper::connect_datapoints_multi(const std::vector<std::string> &data_points) +{ + manager->connect_datapoints_multiple(data_points); +} + +void WinCCWrapper::disconnect_datapoints(const std::vector<std::string> &data_points) +{ + manager->connect_datapoints(data_points); +} + + +void WinCCWrapper::wait_for_event(long sec, long microSec){ + manager->wait_for_event(sec, microSec); +} + void WinCCWrapper::set_connect_datapoints_callback(std::function<void(std::string name, std::string value)> callback) { manager->set_connect_datapoints_callback(callback); } +void WinCCWrapper::set_connect_datapoints_callback(std::function<void(std::map<std::string, std::string>)> callback) +{ + manager->set_connect_datapoints_callback(callback); +} + // set_datapoint bool WinCCWrapper::set_datapoint(const std::string &name, int value, bool valid) { @@ -99,7 +128,7 @@ bool WinCCWrapper::set_datapoint(const std::string &name, boost::python::tuple & bool WinCCWrapper::set_datapoint(const std::string &name, std::vector<int> &value, bool valid) { DynVar variable(VariableType::INTEGER_VAR); - + for(auto iter = value.cbegin(); iter != value.cend(); iter++) { IntegerVar elem{*iter}; variable.append(elem);} @@ -142,6 +171,12 @@ bool WinCCWrapper::set_datapoint_time(const std::string &name, time_t value, boo return manager->set_datapoint(name + DP_SUFFIX, variable, valid); } +boost::any WinCCWrapper::get_datapoint_any (const std::string &name){ + boost::any datapoint_value; + manager->get_datapoint_any(name + DP_SUFFIX, datapoint_value); + return datapoint_value; +} + // get_datapoint template <typename T> bool WinCCWrapper::get_datapoint(const std::string &name, T &value) @@ -213,6 +248,21 @@ time_t WinCCWrapper::get_datapoint_time(const std::string &name) throw std::runtime_error("Could not get datapoint"); } +std::string WinCCWrapper::get_formatted_datapoint(const std::string &name){ + std::string value; + if(manager->get_datapoint(name, value)){ + return value; + } + throw std::runtime_error("Could not get datapoint"); +} + +time_t WinCCWrapper::get_datapoint_set_time(const std::string &name){ + struct tm value; + if(manager->get_datapoint(name + DP_SUFFIX_STIME, value)) + return mktime(&value); + throw std::runtime_error("Could not get datapoint"); +} + bool WinCCWrapper::set_datapoint_valid(const std::string &name) { return manager->set_datapoint_valid(name + DP_SUFFIX); @@ -223,5 +273,8 @@ bool WinCCWrapper::set_datapoint_invalid(const std::string &name) return manager->set_datapoint_invalid(name + DP_SUFFIX); } +bool WinCCWrapper::set_datapoint_any(const std::string &name, boost::any value){ + return manager->set_datapoint_any(name + DP_SUFFIX, value); +} } // namespace WINCCWRAPPER } // namespace LOFAR diff --git a/LCS/WinCCWrapper/src/exceptions.h b/LCS/WinCCWrapper/src/exceptions.h new file mode 100644 index 0000000000000000000000000000000000000000..cf0da769433fabc453caea25a37d76f885ff211f --- /dev/null +++ b/LCS/WinCCWrapper/src/exceptions.h @@ -0,0 +1,12 @@ +class DatapointNameNotFound : public std::exception +{ +private: + std::string message; +public: + DatapointNameNotFound(const std::string & datapointName): + message{"Datapoint " + datapointName + " not found"}{} + const char * what () const throw () + { + return message.c_str(); + } +} diff --git a/LCS/WinCCWrapper/test/CMakeLists.txt b/LCS/WinCCWrapper/test/CMakeLists.txt index 93e3e67b7f1dd434c11c41fd399d18972c55937f..08c5036d1acb50498adbf4f295eefeb1bc8b999c 100644 --- a/LCS/WinCCWrapper/test/CMakeLists.txt +++ b/LCS/WinCCWrapper/test/CMakeLists.txt @@ -8,6 +8,15 @@ IF(BUILD_TESTING) lofar_add_bin_program(WinCCSet WinCCSet.cc) lofar_add_bin_program(WinCCGet WinCCGet.cc) + lofar_add_bin_program(WinCCQuery WinCCQuery.cc) + find_package(Boost COMPONENTS program_options system REQUIRED) + + target_link_libraries(WinCCQuery + wincc_wrapper + ${Boost_LIBRARIES} + + ) + ENDIF(WINCC_FOUND) ENDIF(BUILD_TESTING) diff --git a/LCS/WinCCWrapper/test/WinCCGet.cc b/LCS/WinCCWrapper/test/WinCCGet.cc index 5958d82e915ae681f7e11d206b062c0eebeb1983..aa10b76c422d4866cbec3a7e3bfa14e29dc745ae 100644 --- a/LCS/WinCCWrapper/test/WinCCGet.cc +++ b/LCS/WinCCWrapper/test/WinCCGet.cc @@ -3,28 +3,28 @@ #include <WinCCWrapper.h> #include <vector> #include <iostream> +#include <Resources.hxx> using namespace LOFAR::WINCCWRAPPER; using namespace std; void get_help(){ cout << "Usage:" << endl; - cout << "WinCCGet \"datapoint_name\" datapoint_type" << endl; + cout << "WinCCGet \"datapoint_name\" datapoint_type project_name" << endl; cout << "Accepted datapoint types:" << endl; cout << " int, float, string, list (for int lists)" << endl; } int main(int argc, char * argv[]) { - bool asking_for_help = ((argc == 2) && (string(argv[1]) == "--help" || string(argv[1]) == "--h")); - bool invalid_args = (argc != 3); + bool asking_for_help = ((argc < 4) && (string(argv[1]) == "--help" || string(argv[1]) == "--h")); + bool invalid_args = (argc != 4); if (asking_for_help || invalid_args){ get_help(); return 0; } - - WinCCWrapper wrapper{""}; + WinCCWrapper wrapper{std::string(argv[3])}; string dpname{argv[1]}; if (string(argv[2]) == "int") { diff --git a/LCS/WinCCWrapper/test/WinCCQuery.cc b/LCS/WinCCWrapper/test/WinCCQuery.cc new file mode 100644 index 0000000000000000000000000000000000000000..108c626d304c56e1cb22ecbd3075328732488cee --- /dev/null +++ b/LCS/WinCCWrapper/test/WinCCQuery.cc @@ -0,0 +1,68 @@ + + + +#include <cstdlib> +#include <string> +#include <WinCCManager.h> +#include <WinCCResources.h> + +#include <vector> +#include <iostream> +#include <Resources.hxx> + +#include <boost/program_options.hpp> +#include <boost/any.hpp> + + + +using namespace LOFAR::WINCCWRAPPER; +using namespace std; +namespace po = boost::program_options; + +void set_options(po::options_description & description){ + description.add_options() + ("help", "produce help message") + ("sql", po::value<std::string>(), "SQL_query") + ("project", po::value<std::string>(), "WinCC project"); + +} + +int main(int argc, char * argv[]) +{ + // Declare the supported options. + po::options_description desc("Query the WinCCDatabase"); + set_options(desc); + + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + + if (vm.count("help")) { + cout << desc << "\n"; + return 1; + } + + if (vm.count("sql") == 1 && vm.count("project") == 1) { + + const string sql{vm["sql"].as<string>()}; + + WinCCResources resource{"WinCCQuery", vm["project"].as<string>(), 0}; + WinCCManager manager; + cout << "The SQL is: [" << sql << "].\n"; + + std::vector<std::vector<std::string>> queryResult; + manager.get_query(sql, queryResult); + std::cout<< "RESULTS ----"<<"\n\n"; + std::cout<< "datapoint" << "\t"; + for(auto & row : queryResult){ + for(std::string & column : row){ + std::cout<<column<<"\t"; + } + std::cout<<"\n"; + } + + } else { + cout << desc; + } +} diff --git a/LCU/Maintenance/DBInterface/django_postgresql/settings.py b/LCU/Maintenance/DBInterface/django_postgresql/settings.py index 12b5486b5c1efb62ee61a3f2b6536cfec295f33b..b3e57efb1e9c550ca604173a8cb2ccace57eba74 100644 --- a/LCU/Maintenance/DBInterface/django_postgresql/settings.py +++ b/LCU/Maintenance/DBInterface/django_postgresql/settings.py @@ -30,7 +30,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY = 'x)5=9*2a&)32h-loh@rlt_9eyw%t$-fqao*#1j2gh^7=^bnjyy' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True ALLOWED_HOSTS = ['lofarmonitortest.control.lofar', 'localhost', '127.0.0.1'] @@ -47,9 +47,11 @@ INSTALLED_APPS = [ 'rest_framework', 'django_filters', 'lofar.maintenance.monitoringdb.apps.MonitoringDbConfig', + 'silk' ] MIDDLEWARE = [ + 'silk.middleware.SilkyMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -60,7 +62,8 @@ MIDDLEWARE = [ ] ROOT_URLCONF = 'lofar.maintenance.django_postgresql.urls' - +SILKY_PYTHON_PROFILER = True +SILKY_PYTHON_PROFILER_BINARY = True TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -114,9 +117,9 @@ LOGGING = { 'handlers': ['console'], 'level': 'DEBUG' }, - 'django.db.backends':{ + 'django.db.backends': { 'handlers': ['console'], - 'level': 'DEBUG' + 'level': 'INFO' } }, } @@ -188,8 +191,9 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ - -STATIC_URL = 'static/' +PATH = '/home/mmancini/svn-tree/MonitoringMaintenance-OSB-45/build/gnucxx11_debug' +STATIC_ROOT = os.path.join(PATH, "static") +STATIC_URL = '/static/' REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'lofar.maintenance.monitoringdb.pagination.DefaultPaginationSettings', @@ -199,4 +203,4 @@ REST_FRAMEWORK = { CELERY_RESULT_BACKEND = 'amqp://guest@localhost//' # LOFAR SPECIFIC PARAMETERS -URL_TO_STORE_RTSM_PLOTS = '/data/deploy/production/rtsm_plots' \ No newline at end of file +URL_TO_STORE_RTSM_PLOTS = PATH \ No newline at end of file diff --git a/LCU/Maintenance/DBInterface/django_postgresql/urls.py b/LCU/Maintenance/DBInterface/django_postgresql/urls.py index 1d14e92ffba1fe5f039e0446493aaf16c8dd035d..d024aff41f84182fdd7c179d98f9cb22f1de520f 100644 --- a/LCU/Maintenance/DBInterface/django_postgresql/urls.py +++ b/LCU/Maintenance/DBInterface/django_postgresql/urls.py @@ -14,9 +14,15 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.conf.urls import include -from django.urls import path +from django.conf.urls import include, url + +from django.conf import settings +from django.conf.urls.static import static + urlpatterns = [ - path('', include('lofar.maintenance.monitoringdb.urls')) -] +# path('', include('lofar.maintenance.monitoringdb.urls')) +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +from lofar.maintenance.monitoringdb.urls import urlpatterns as urls +urlpatterns += urls +urlpatterns += [url(r'^silk/', include('silk.urls', namespace='silk'))] \ No newline at end of file diff --git a/LCU/Maintenance/DBInterface/monitoringdb/migrations/0001_initial.py b/LCU/Maintenance/DBInterface/monitoringdb/migrations/0001_initial.py deleted file mode 100644 index 187852808bdf87e771a4f1be02dc26d7314ce2a7..0000000000000000000000000000000000000000 --- a/LCU/Maintenance/DBInterface/monitoringdb/migrations/0001_initial.py +++ /dev/null @@ -1,159 +0,0 @@ -# Generated by Django 2.0.7 on 2019-01-09 14:44 - -import django.contrib.postgres.fields -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='ActionLog', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('entry_id', models.IntegerField(help_text='database id of the concerned object')), - ('model_name', models.CharField(max_length=100)), - ('who', models.CharField(max_length=100)), - ('when', models.DateTimeField(auto_now_add=True)), - ('what', models.CharField(max_length=1000)), - ], - ), - migrations.CreateModel( - name='Component', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(max_length=50)), - ('component_id', models.IntegerField()), - ], - ), - migrations.CreateModel( - name='ComponentError', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(max_length=50)), - ('details', django.contrib.postgres.fields.jsonb.JSONField(default=dict)), - ('component', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Component')), - ], - ), - migrations.CreateModel( - name='Element', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('element_id', models.PositiveIntegerField()), - ('component', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Component')), - ], - ), - migrations.CreateModel( - name='ElementError', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(max_length=50)), - ('details', django.contrib.postgres.fields.jsonb.JSONField(default=dict)), - ('component_error', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='failing_elements', to='monitoringdb.ComponentError')), - ('element', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Element')), - ], - ), - migrations.CreateModel( - name='RTSMError', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rcu', models.SmallIntegerField(default=None, null=True)), - ('mode', models.SmallIntegerField(default=None, null=True)), - ('error_type', models.CharField(max_length=50)), - ('start_frequency', models.FloatField(default=None, null=True)), - ('stop_frequency', models.FloatField(default=None, null=True)), - ('time', models.DateTimeField()), - ], - ), - migrations.CreateModel( - name='RTSMErrorSummary', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('percentage', models.FloatField(default=None, null=True)), - ('rcu', models.SmallIntegerField(default=None, null=True)), - ('mode', models.SmallIntegerField(default=None, null=True)), - ('count', models.IntegerField(default=None, null=True)), - ('error_type', models.CharField(max_length=50)), - ('start_frequency', models.FloatField(default=None, null=True)), - ('stop_frequency', models.FloatField(default=None, null=True)), - ], - ), - migrations.CreateModel( - name='RTSMObservation', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('observation_id', models.PositiveIntegerField(default=0)), - ('start_datetime', models.DateTimeField()), - ('end_datetime', models.DateTimeField()), - ('samples', models.IntegerField(default=None, null=True)), - ], - ), - migrations.CreateModel( - name='RTSMSpectrum', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('bad_spectrum', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(), size=None)), - ('average_spectrum', django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(), size=None)), - ('rtsm_error', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='monitoringdb.RTSMError')), - ], - ), - migrations.CreateModel( - name='RTSMSummaryPlot', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uri', models.CharField(default=None, max_length=10000, null=True)), - ('error_summary', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='summary_plot', to='monitoringdb.RTSMErrorSummary')), - ], - ), - migrations.CreateModel( - name='Station', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('location', models.CharField(blank=True, help_text='where the station is located', max_length=50, null=True)), - ('name', models.CharField(help_text='name of the station', max_length=10)), - ('type', models.CharField(choices=[('c', 'core'), ('i', 'international'), ('r', 'remote')], help_text='station type one of [Core[C], Remote[R], International[I]]', max_length=1)), - ], - ), - migrations.CreateModel( - name='StationTest', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start_datetime', models.DateTimeField()), - ('end_datetime', models.DateTimeField()), - ('checks', models.CharField(max_length=2000)), - ('station', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Station')), - ], - ), - migrations.AddField( - model_name='rtsmobservation', - name='station', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='monitoringdb.Station'), - ), - migrations.AddField( - model_name='rtsmerrorsummary', - name='observation', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='errors_summary', to='monitoringdb.RTSMObservation'), - ), - migrations.AddField( - model_name='rtsmerror', - name='observation', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='errors', to='monitoringdb.RTSMObservation'), - ), - migrations.AddField( - model_name='componenterror', - name='station_test', - field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='component_errors', to='monitoringdb.StationTest'), - ), - migrations.AddField( - model_name='component', - name='station', - field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='monitoringdb.Station'), - ), - ] diff --git a/LCU/Maintenance/DBInterface/monitoringdb/migrations/0002_winccantennastatuses.py b/LCU/Maintenance/DBInterface/monitoringdb/migrations/0002_winccantennastatuses.py deleted file mode 100644 index d75d4485685f9fa63b564a244d09b6c2d4086b35..0000000000000000000000000000000000000000 --- a/LCU/Maintenance/DBInterface/monitoringdb/migrations/0002_winccantennastatuses.py +++ /dev/null @@ -1,86 +0,0 @@ -# Generated by Django 2.0.7 on 2019-03-07 16:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('monitoringdb', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='WinCCAntennaStatus', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('station', models.CharField(max_length=10)), - ('antenna_type', models.CharField(max_length=4)), - ('antenna_id', models.IntegerField()), - ('status_code', models.IntegerField()), - ('status_code_reason', models.CharField(max_length=1000)), - ('status_code_modified_at', models.CharField(max_length=100)), - ('element_00', models.IntegerField(null=True)), - ('element_00_reason', models.CharField(max_length=1000, null=True)), - ('element_00_modified_at', models.CharField(max_length=100, null=True)), - ('element_01', models.IntegerField(null=True)), - ('element_01_reason', models.CharField(max_length=1000, null=True)), - ('element_01_modified_at', models.CharField(max_length=100, null=True)), - ('element_02', models.IntegerField(null=True)), - ('element_02_reason', models.CharField(max_length=1000, null=True)), - ('element_02_modified_at', models.CharField(max_length=100, null=True)), - ('element_03', models.IntegerField(null=True)), - ('element_03_reason', models.CharField(max_length=1000, null=True)), - ('element_03_modified_at', models.CharField(max_length=100, null=True)), - ('element_04', models.IntegerField(null=True)), - ('element_04_reason', models.CharField(max_length=1000, null=True)), - ('element_04_modified_at', models.CharField(max_length=100, null=True)), - ('element_05', models.IntegerField(null=True)), - ('element_05_reason', models.CharField(max_length=1000, null=True)), - ('element_05_modified_at', models.CharField(max_length=100, null=True)), - ('element_06', models.IntegerField(null=True)), - ('element_06_reason', models.CharField(max_length=1000, null=True)), - ('element_06_modified_at', models.CharField(max_length=100, null=True)), - ('element_07', models.IntegerField(null=True)), - ('element_07_reason', models.CharField(max_length=1000, null=True)), - ('element_07_modified_at', models.CharField(max_length=100, null=True)), - ('element_08', models.IntegerField(null=True)), - ('element_08_reason', models.CharField(max_length=1000, null=True)), - ('element_08_modified_at', models.CharField(max_length=100, null=True)), - ('element_09', models.IntegerField(null=True)), - ('element_09_reason', models.CharField(max_length=1000, null=True)), - ('element_09_modified_at', models.CharField(max_length=100, null=True)), - ('element_10', models.IntegerField(null=True)), - ('element_10_reason', models.CharField(max_length=1000, null=True)), - ('element_10_modified_at', models.CharField(max_length=100, null=True)), - ('element_11', models.IntegerField(null=True)), - ('element_11_reason', models.CharField(max_length=1000, null=True)), - ('element_11_modified_at', models.CharField(max_length=100, null=True)), - ('element_12', models.IntegerField(null=True)), - ('element_12_reason', models.CharField(max_length=1000, null=True)), - ('element_12_modified_at', models.CharField(max_length=100, null=True)), - ('element_13', models.IntegerField(null=True)), - ('element_13_reason', models.CharField(max_length=1000, null=True)), - ('element_13_modified_at', models.CharField(max_length=100, null=True)), - ('element_14', models.IntegerField(null=True)), - ('element_14_reason', models.CharField(max_length=1000, null=True)), - ('element_14_modified_at', models.CharField(max_length=100, null=True)), - ('element_15', models.IntegerField(null=True)), - ('element_15_reason', models.CharField(max_length=1000, null=True)), - ('element_15_modified_at', models.CharField(max_length=100, null=True)), - ], - options={ - 'db_table': 'antenna_statuses', - 'ordering': ['-timestamp', 'station', 'antenna_type', 'antenna_id'], - }, - ), - migrations.AddIndex( - model_name='winccantennastatus', - index=models.Index(fields=['station', 'antenna_type', 'antenna_id', 'timestamp'], name='antenna_sta_station_2632f8_idx'), - ), - migrations.RunSQL( - sql="ALTER TABLE antenna_statuses ALTER COLUMN timestamp SET DEFAULT now()", - reverse_sql="" - ) - ] diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/component.py b/LCU/Maintenance/DBInterface/monitoringdb/models/component.py index a8bf7a0a9a6cb509e37cb09341b92f996b8a2359..7995893b53995ea3b76898608b6e042bc4c9c4aa 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/models/component.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/component.py @@ -1,11 +1,33 @@ from django.db import models from .station import Station +from typing import Tuple + + +HBA_ELEMENT_ID_RANGE = range(0, 16, 1) + +def antenna_type_antenna_id_to_component_type_component_id(station_name, + antenna_type, + antenna_id) -> Tuple[str, int]: + if antenna_type == 'HBA': + return 'HBA', antenna_id + + if station_name.startswith('CS') or station_name.startswith('RS'): + if antenna_id >= 48: + component_type = 'LBL' + component_id = antenna_id - 48 + else: + component_type = 'LBH' + component_id = antenna_id + + return component_type, component_id + else: + return 'LBH', antenna_id class Component(models.Model): type = models.CharField(max_length=50) component_id = models.IntegerField() - station = models.ForeignKey(Station, on_delete=models.DO_NOTHING) + station = models.ForeignKey(Station, on_delete=models.DO_NOTHING, related_name='components') class Meta: unique_together = ('station', 'type', 'component_id') diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/element.py b/LCU/Maintenance/DBInterface/monitoringdb/models/element.py index 708adfc1fff162392a7af8c1806e2aaa946f43bc..3694fb3b0c5822acea0cee6799654cdd09cb4790 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/models/element.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/element.py @@ -3,7 +3,7 @@ from .component import Component class Element(models.Model): - component = models.ForeignKey(Component, on_delete=models.DO_NOTHING) + component = models.ForeignKey(Component, on_delete=models.DO_NOTHING, related_name='elements') element_id = models.PositiveIntegerField() class Meta: diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/rtsm.py b/LCU/Maintenance/DBInterface/monitoringdb/models/rtsm.py index c50dda3dad6a724554253e24df3f7a70264edc12..af3101d631b893ab752620ee42390a8b1cb843a6 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/models/rtsm.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/rtsm.py @@ -3,8 +3,9 @@ In this file all the model regarding the real time station monitor output are co """ from django.db import models from django.contrib.postgres.fields import ArrayField -from .station import Station + from .component import Component +from .test import GenericTest MODE_TO_COMPONENT = { 1: 'LBL', @@ -16,38 +17,80 @@ MODE_TO_COMPONENT = { 7: 'HBA' } -COMPONENT_TO_MODE = { - 'LBL': [1, 2], - 'LBH': [3, 4], - 'HBA': [5, 6, 7] -} - -class RTSMObservation(models.Model): - observation_id = models.PositiveIntegerField(default=0) +def antenna_id_polarization_from_rcu_type(rcu, type): + """ + Compute the antenna id for a given rcu, type and polarization + :param rcu: id of the rcu + :param type: type of the antenna + :return: the antenna id and polarization + :rtype: (int, str) + """ + polarization_index = rcu % 2 - station = models.ForeignKey(Station, on_delete=models.SET_NULL, null=True) + if type in ['LBH', 'HBA']: + antenna_id = rcu - polarization_index + antenna_id /= 2. + polarization = 'Y' if polarization_index > 0 else 'X' + elif type == 'LBL': + antenna_id = (rcu - polarization_index) / 2. + 48 + polarization = 'X' if polarization_index > 0 else 'Y' + else: + antenna_id = -1 + polarization = '' + return antenna_id, polarization - start_datetime = models.DateTimeField() - end_datetime = models.DateTimeField() - samples = models.IntegerField(default=None, null=True) +class RTSMObservation(GenericTest): + sas_id = models.PositiveIntegerField(default=0) + samples = models.PositiveIntegerField(default=0) class RTSMError(models.Model): observation = models.ForeignKey(RTSMObservation, related_name='errors', on_delete=models.CASCADE) - component = models.ForeignKey(Component, on_delete=models.SET_NULL) + component = models.ForeignKey(Component, on_delete=models.SET_NULL, null=True) + count = models.PositiveIntegerField(default=0, null=True) rcu = models.SmallIntegerField(default=None, null=True) mode = models.SmallIntegerField(default=None, null=True) percentage = models.FloatField(default=None, null=True) - + polarization = models.CharField(max_length=1) error_type = models.CharField(max_length=50) + @staticmethod + def get_or_create_error_from_rtsm_result(observation: RTSMObservation, + rcu, + mode, + percentage, + error_type, + count): + station = observation.station + component_type = MODE_TO_COMPONENT[mode] + component_id, polarization = antenna_id_polarization_from_rcu_type(rcu, component_type) + component_entity, _ = Component.objects.get_or_create(station=station, + type=component_type, + component_id=component_id) + rtsm_error, created = RTSMError.objects.get_or_create(observation=observation, + component=component_entity, + rcu=rcu, + mode=mode, + polarization=polarization, + error_type=error_type, + percentage=percentage, + count=count + ) + + return rtsm_error, created + class RTSMErrorSample(models.Model): time = models.DateTimeField() error = models.ForeignKey(RTSMError, related_name='samples', on_delete=models.CASCADE) - observation_id = models.ForeignKey(RTSMObservation, on_delete=models.CASCADE) + + start_frequency = models.FloatField(default=None, null=True) + stop_frequency = models.FloatField(default=None, null=True) + + bad_spectrum = ArrayField(models.FloatField()) + average_spectrum = ArrayField(models.FloatField()) class RTSMSummaryPlot(models.Model): @@ -58,11 +101,3 @@ class RTSMSummaryPlot(models.Model): uri = models.CharField(max_length=10000, default=None, null=True) - -class RTSMSpectrum(models.Model): - start_frequency = models.FloatField(default=None, null=True) - stop_frequency = models.FloatField(default=None, null=True) - - bad_spectrum = ArrayField(models.FloatField()) - average_spectrum = ArrayField(models.FloatField()) - rtsm_error = models.ForeignKey(RTSMErrorSample, related_name='spectra', on_delete=models.CASCADE) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/signals/generic_test_insert.py b/LCU/Maintenance/DBInterface/monitoringdb/models/signals/generic_test_insert.py new file mode 100644 index 0000000000000000000000000000000000000000..9a4ae82847bb4178251aab50210a6b4f0743192a --- /dev/null +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/signals/generic_test_insert.py @@ -0,0 +1,80 @@ +from typing import List + +from ..component import HBA_ELEMENT_ID_RANGE, Component, \ + antenna_type_antenna_id_to_component_type_component_id +from ..element import Element +from ..test import GenericTest +from ..wincc import ElementStatus, \ + ComponentStatus, \ + StationStatus, WinCCAntennaStatus, STATUS_CODE_TO_STATUS, latest_status_per_station_raw + + +def insert_hba_element_status(component_status: ComponentStatus, + hba_wincc_status: WinCCAntennaStatus): + element_statuses = [] + for element_id in HBA_ELEMENT_ID_RANGE: + element_status_field = 'element_{:02d}'.format(element_id) + element_status_code = hba_wincc_status.serializable_value(element_status_field) + element_status_description = STATUS_CODE_TO_STATUS[element_status_code] + element_status_message = hba_wincc_status.serializable_value(element_status_field + + '_reason') + element_status_modified_at = hba_wincc_status.serializable_value(element_status_field + + '_modified_at') + + element, created = Element.objects.get_or_create(element_id=element_id, + component=component_status.component) + element_status = ElementStatus( + component_status=component_status, + status_code=element_status_code, + status_description=element_status_description, + status_message=element_status_message, + modified_at=element_status_modified_at, + element=element) + element_statuses.append(element_status) + + return ElementStatus.objects.bulk_create(element_statuses) + + +def insert_component_status(station_status: StationStatus, component_status: WinCCAntennaStatus): + station = station_status.station + component_type, component_id = antenna_type_antenna_id_to_component_type_component_id( + station.name, + component_status.antenna_type, + component_status.antenna_id) + + component, created = Component.objects.get_or_create(station=station, + type=component_type, + component_id=component_id) + component_status_entity = ComponentStatus( + component=component, + status_code=component_status.status_code, + status_description=STATUS_CODE_TO_STATUS[ + component_status.status_code], + status_message=component_status.status_code_reason, + modified_at=component_status.status_code_modified_at, + station_status=station_status) + component_status_entity.save() + + if component_status.antenna_type == 'HBA': + insert_hba_element_status(component_status_entity, component_status) + return component_status + + +def insert_station_status(test_instance: GenericTest, statuses: List[WinCCAntennaStatus]): + station_status_instance = StationStatus(station=test_instance.station) + station_status_instance.save() + + test_instance.station_status = station_status_instance + for status in statuses: + insert_component_status(station_status_instance, status) + + +def on_inserting_test(sender, instance: GenericTest, **kwargs): + logger.info('received post_insert signal from %s for instance %s', sender, instance) + queryset = WinCCAntennaStatus.objects.all().order_by() + query_results = [wincc_status for wincc_status in + latest_status_per_station_raw(queryset=queryset, + station=instance.station.name, + to_date=instance.start_datetime)] + + insert_station_status(instance, query_results) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/station_test.py b/LCU/Maintenance/DBInterface/monitoringdb/models/station_test.py index 9d7ff07d90ca1d76fb2f49044c85a353f5e8f97b..29d982eabb77edf01db8090125e3371ee471a58e 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/models/station_test.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/station_test.py @@ -1,10 +1,8 @@ from django.db import models -from .station import Station +from .test import GenericTest -class StationTest(models.Model): - start_datetime = models.DateTimeField() - end_datetime = models.DateTimeField() + +class StationTest(GenericTest): checks = models.CharField(max_length=2000) - station = models.ForeignKey(Station, on_delete=models.DO_NOTHING) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/test.py b/LCU/Maintenance/DBInterface/monitoringdb/models/test.py new file mode 100644 index 0000000000000000000000000000000000000000..b7565bde2e2c2d96c244861448e78d1cead3b48c --- /dev/null +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/test.py @@ -0,0 +1,19 @@ +import logging + +from django.db import models + +from .station import Station +from .wincc import StationStatus + +logger = logging.getLogger(__name__) + + +class GenericTest(models.Model): + start_datetime = models.DateTimeField() + end_datetime = models.DateTimeField() + + station = models.ForeignKey(Station, on_delete=models.DO_NOTHING, related_name='tests') + + station_status = models.OneToOneField(StationStatus, + on_delete=models.CASCADE, + null=True) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/models/wincc.py b/LCU/Maintenance/DBInterface/monitoringdb/models/wincc.py index 12d5b6aee7778d321223e36b7a9ba8be8f07ade0..0e9d2bef37bb535f50a92d47e3a580f08affef8f 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/models/wincc.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/models/wincc.py @@ -1,16 +1,20 @@ -import django.db.models as models from collections import OrderedDict +import django.db.models as models + +from .component import Component +from .element import Element +from .station import Station COMPONENT_TYPE_TO_ANTENNA_TYPE = dict(HBA='HBA', LBH='LBA', LBL='LBA') -STATUS_CODE_TO_STATUS = {0:'OFF', - 10:'OPERATIONAL', - 20:'MAINTENANCE', - 30:'TEST', - 40:'SUSPICIOUS', - 50:'BROKEN', - 60:'BEYOND REPAIR', - 70:'MISSING DATAPOINT'} +STATUS_CODE_TO_STATUS = {0: 'OFF', + 10: 'OPERATIONAL', + 20: 'MAINTENANCE', + 30: 'TEST', + 40: 'SUSPICIOUS', + 50: 'BROKEN', + 60: 'BEYOND REPAIR', + 70: 'MISSING DATAPOINT'} def to_antenna_type(component_type): @@ -49,6 +53,33 @@ def _format_result(raw_query_result, antenna_type): return results +def latest_status_per_station_raw(queryset, station, to_date): + """ + Retrieve the latest antenna statuses for a given station and antenna type + :param queryset: source queryset + :param station: station name + :type station: str + :param component_type: component type name + :type component_type: str + :param to_date: select the latest test before the to date + :type to_date: datetime.dattime + :return: the status of an antenna per antenna id + """ + raw_results = queryset.raw(''' + SELECT * + FROM antenna_statuses master + INNER JOIN ( + SELECT antenna_type, antenna_id, MAX("timestamp") last_entry_timestamp + FROM antenna_statuses + WHERE timestamp <= %(to_date)s AND station=%(station_name)s + GROUP BY antenna_type, antenna_id + ) slave ON master.antenna_type=slave.antenna_type AND master.antenna_id = slave.antenna_id AND last_entry_timestamp=master.timestamp + WHERE master.station=%(station_name)s + ''', dict(to_date=to_date, station_name=station.rstrip('C'))) + + return raw_results + + def latest_status_per_station_and_antenna_type(queryset, station, antenna_type, to_date): """ Retrieve the latest antenna statuses for a given station and antenna type @@ -65,10 +96,10 @@ def latest_status_per_station_and_antenna_type(queryset, station, antenna_type, SELECT * FROM antenna_statuses master INNER JOIN ( - SELECT station, antenna_type, antenna_id, MAX("timestamp") last_entry_timestamp + SELECT antenna_type, antenna_id, MAX("timestamp") last_entry_timestamp FROM antenna_statuses - WHERE timestamp <= %s - GROUP BY station, antenna_type, antenna_id + WHERE timestamp <= %s AND station=%s + GROUP BY antenna_type, antenna_id ) slave ON master.station=slave.station AND master.antenna_type=slave.antenna_type AND master.antenna_id = slave.antenna_id AND last_entry_timestamp=master.timestamp WHERE master.station=%s AND master.antenna_type=%s ''', [to_date, station.rstrip('C'), antenna_type]) @@ -91,7 +122,7 @@ def latest_status_per_station_and_component_type(queryset, station, component_ty antenna_type = to_antenna_type(component_type) if antenna_type: return latest_status_per_station_and_antenna_type(queryset, station, antenna_type, - to_date) + to_date) else: return OrderedDict() @@ -113,9 +144,10 @@ def latest_status_per_station_and_component_type_antenna_id(queryset, station, """ antenna_type = to_antenna_type(component_type) if antenna_type: - return latest_status_per_station_and_antenna_type_antenna_id(queryset, station, antenna_type, - component_id, - to_date) + return latest_status_per_station_and_antenna_type_antenna_id(queryset, station, + antenna_type, + component_id, + to_date) else: return OrderedDict() @@ -129,8 +161,8 @@ def latest_status_per_station_and_antenna_type_antenna_id(queryset, station, :param queryset: source queryset :param station: station name :type station: str - :param component_type: component type name - :type component_type: str + :param antenna_type: antenna type name + :type antenna_type: str :param to_date: select the latest test before the to date :type to_date: datetime.datetime :return: the status of an antenna per antenna id @@ -164,69 +196,92 @@ class WinCCAntennaStatus(models.Model): status_code = models.IntegerField() status_code_reason = models.CharField(max_length=1000) - status_code_modified_at = models.CharField(max_length=100) + status_code_modified_at = models.DateTimeField(max_length=100) element_00 = models.IntegerField(null=True) element_00_reason = models.CharField(max_length=1000, null=True) - element_00_modified_at = models.CharField(max_length=100, null=True) + element_00_modified_at = models.DateTimeField(max_length=100, null=True) element_01 = models.IntegerField(null=True) element_01_reason = models.CharField(max_length=1000, null=True) - element_01_modified_at = models.CharField(max_length=100, null=True) + element_01_modified_at = models.DateTimeField(max_length=100, null=True) element_02 = models.IntegerField(null=True) element_02_reason = models.CharField(max_length=1000, null=True) - element_02_modified_at = models.CharField(max_length=100, null=True) + element_02_modified_at = models.DateTimeField(max_length=100, null=True) element_03 = models.IntegerField(null=True) element_03_reason = models.CharField(max_length=1000, null=True) - element_03_modified_at = models.CharField(max_length=100, null=True) + element_03_modified_at = models.DateTimeField(max_length=100, null=True) element_04 = models.IntegerField(null=True) element_04_reason = models.CharField(max_length=1000, null=True) - element_04_modified_at = models.CharField(max_length=100, null=True) + element_04_modified_at = models.DateTimeField(max_length=100, null=True) element_05 = models.IntegerField(null=True) element_05_reason = models.CharField(max_length=1000, null=True) - element_05_modified_at = models.CharField(max_length=100, null=True) + element_05_modified_at = models.DateTimeField(max_length=100, null=True) element_06 = models.IntegerField(null=True) element_06_reason = models.CharField(max_length=1000, null=True) - element_06_modified_at = models.CharField(max_length=100, null=True) + element_06_modified_at = models.DateTimeField(max_length=100, null=True) element_07 = models.IntegerField(null=True) element_07_reason = models.CharField(max_length=1000, null=True) - element_07_modified_at = models.CharField(max_length=100, null=True) + element_07_modified_at = models.DateTimeField(max_length=100, null=True) element_08 = models.IntegerField(null=True) element_08_reason = models.CharField(max_length=1000, null=True) - element_08_modified_at = models.CharField(max_length=100, null=True) + element_08_modified_at = models.DateTimeField(max_length=100, null=True) element_09 = models.IntegerField(null=True) element_09_reason = models.CharField(max_length=1000, null=True) - element_09_modified_at = models.CharField(max_length=100, null=True) + element_09_modified_at = models.DateTimeField(max_length=100, null=True) element_10 = models.IntegerField(null=True) element_10_reason = models.CharField(max_length=1000, null=True) - element_10_modified_at = models.CharField(max_length=100, null=True) + element_10_modified_at = models.DateTimeField(max_length=100, null=True) element_11 = models.IntegerField(null=True) element_11_reason = models.CharField(max_length=1000, null=True) - element_11_modified_at = models.CharField(max_length=100, null=True) + element_11_modified_at = models.DateTimeField(max_length=100, null=True) element_12 = models.IntegerField(null=True) element_12_reason = models.CharField(max_length=1000, null=True) - element_12_modified_at = models.CharField(max_length=100, null=True) + element_12_modified_at = models.DateTimeField(max_length=100, null=True) element_13 = models.IntegerField(null=True) element_13_reason = models.CharField(max_length=1000, null=True) - element_13_modified_at = models.CharField(max_length=100, null=True) + element_13_modified_at = models.DateTimeField(max_length=100, null=True) element_14 = models.IntegerField(null=True) element_14_reason = models.CharField(max_length=1000, null=True) - element_14_modified_at = models.CharField(max_length=100, null=True) + element_14_modified_at = models.DateTimeField(max_length=100, null=True) element_15 = models.IntegerField(null=True) element_15_reason = models.CharField(max_length=1000, null=True) - element_15_modified_at = models.CharField(max_length=100, null=True) + element_15_modified_at = models.DateTimeField(max_length=100, null=True) + + +class EntityStatus(models.Model): + status_code = models.IntegerField(default=0) + status_description = models.CharField(max_length=20) + status_message = models.CharField(max_length=100, default='') + modified_at = models.DateTimeField(null=True, default=None) + + class Meta: + abstract = True + + +class StationStatus(models.Model): + station = models.ForeignKey(Station, on_delete=models.DO_NOTHING, null=True) + + +class ComponentStatus(EntityStatus): + station_status = models.ForeignKey(StationStatus, on_delete=models.CASCADE) + component = models.ForeignKey(Component, on_delete=models.DO_NOTHING) + +class ElementStatus(EntityStatus): + component_status = models.ForeignKey(ComponentStatus, on_delete=models.CASCADE) + element = models.ForeignKey(Element, on_delete=models.DO_NOTHING) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/rtsm_test_raw_parser.py b/LCU/Maintenance/DBInterface/monitoringdb/rtsm_test_raw_parser.py index 04f5f8b589c6acb81888bacd8c2ed91fe0f24307..f1ff3df1e2950c13a42ca847009b0416260e4113 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/rtsm_test_raw_parser.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/rtsm_test_raw_parser.py @@ -1,6 +1,7 @@ from datetime import datetime import pytz + rtsm_error_types_to_error_types = dict(HN='HIGH_NOISE', SN='SUMMATOR_NOISE', CR='CABLE_REFLECTION', @@ -86,17 +87,18 @@ def group_samples_by_error_type(content): average_spectrum=average_spectrum, bad_spectrum=bad_spectrum) if key in grouped_errors: - grouped_errors[key]['n_samples'] += 1 + grouped_errors[key]['count'] += 1 grouped_errors[key]['samples'] += [sample] else: grouped_errors[key] = dict( - n_samples = 1, + count = 1, samples = [sample], **error ) content['errors'] = list(grouped_errors.values()) return content + def parse_rtsm_test(content): """ Expects a string content with the rtsm test output diff --git a/LCU/Maintenance/DBInterface/monitoringdb/serializers/rtsm.py b/LCU/Maintenance/DBInterface/monitoringdb/serializers/rtsm.py index 85f26935647bfa70453c457e49d45f947a95f18f..288f75f13ad07c2449c55588a523ecff15ee9a9a 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/serializers/rtsm.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/serializers/rtsm.py @@ -11,21 +11,7 @@ import os logger = logging.getLogger('serializers') -class RTSMSpectrumSerializer(serializers.ModelSerializer): - bad_spectrum = serializers.ListField(child=serializers.FloatField()) - average_spectrum = serializers.ListField(child=serializers.FloatField()) - - class Meta: - model = RTSMSpectrum - fields = '__all__' - - class RTSMErrorSampleSerializer(serializers.ModelSerializer): - observation = serializers.PrimaryKeyRelatedField(read_only=True) - - def __init__(self, *args, **kwargs): - self.Meta.depth = kwargs.pop('depth', 1) - super(RTSMErrorSampleSerializer, self).__init__(*args, **kwargs) class Meta: model = RTSMErrorSample @@ -34,6 +20,7 @@ class RTSMErrorSampleSerializer(serializers.ModelSerializer): class RTSMErrorSerializer(serializers.ModelSerializer): observation = serializers.PrimaryKeyRelatedField(read_only=True) + samples = serializers.PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = RTSMError @@ -47,62 +34,6 @@ class RTSMObservationSerializer(serializers.ModelSerializer): model = RTSMObservation fields = '__all__' - @staticmethod - def compute_summary(RTSMObservation_entry): - summary_entries = RTSMObservation_entry.errors.defer('spectra'). \ - filter(observation=RTSMObservation_entry).values('error_type', 'mode', 'rcu', 'start_frequency', - 'stop_frequency'). \ - annotate(count=models.Count('error_type', unique=True, output_field=models.FloatField())). \ - annotate(percentage=models.ExpressionWrapper(models.F('count') * 100. / models.F('observation__samples'), - output_field=models.FloatField())) - for summary_entry in summary_entries: - RTSMError(observation=RTSMObservation_entry, **summary_entry).save() - - @atomic - def create(self, validated_data): - rtsm_errors = validated_data.pop('errors') - - # derive the station type from the station name - station_name = validated_data.pop('station_name') - station_type = station_name[0].capitalize() - station_type = station_type if station_type in ['C', 'R'] else 'I' - - station_entry = Station.objects.filter(name=station_name).first() - if station_entry is None: - station_entry = Station(name=station_name, type=station_type) - station_entry.save() - # searches if the RTSM is already present - observation_id = validated_data['observation_id'] - RTSMObservation_instance = RTSMObservation.objects.filter(observation_id=observation_id, - station=station_entry).first() - - if RTSMObservation_instance is None: - logger.info('rtsm not found inserting %s', validated_data) - RTSMObservation_instance = RTSMObservation.objects.create(station=station_entry, - **validated_data) - RTSMObservation_instance.save() - ActionLogSerializer.log_model_insert(RTSMObservation_instance) - else: - raise ItemAlreadyExists(RTSMObservation_instance) - for rtsm_error in rtsm_errors: - rtsm_error.update(observation=RTSMObservation_instance) - print(rtsm_error) - average_spectrum = rtsm_error.pop('average_spectrum') - bad_spectrum = rtsm_error.pop('bad_spectrum') - - rtsm_error_instance = RTSMErrorSample.objects.create(**rtsm_error) - rtsm_error_instance.save() - ActionLogSerializer.log_model_insert(rtsm_error_instance) - - rtsm_spectrum_instance = RTSMSpectrum.objects.create(average_spectrum=average_spectrum, - bad_spectrum=bad_spectrum, - rtsm_error=rtsm_error_instance) - rtsm_spectrum_instance.save() - ActionLogSerializer.log_model_insert(rtsm_spectrum_instance) - self.compute_summary(RTSMObservation_instance) - - return RTSMObservation_instance - class FilePathSerializer(serializers.Field): def __init__(self, path, *args, **kwargs): diff --git a/LCU/Maintenance/DBInterface/monitoringdb/station_test_raw_parser.py b/LCU/Maintenance/DBInterface/monitoringdb/station_test_raw_parser.py index 43cb0c9e0d6068b38a46df60802d6428c8224692..9d69bbad43de598d55d20086cac1afed45c490ee 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/station_test_raw_parser.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/station_test_raw_parser.py @@ -11,6 +11,12 @@ from collections import defaultdict logger = logging.getLogger('station_test.parser') +def station_type_from_station_name(station_name): + station_type = station_name[0].capitalize() + station_type = station_type if station_type in ['C', 'R'] else 'I' + return station_type + + def parse_key_value_pairs(content): """ Parse a key value pair returning a dict. @@ -81,6 +87,8 @@ def split_history_into_tests(content): all_tests = [] current_test = [] for i, line in enumerate(content[:-1]): + if line.startswith('#'): + continue next_line_columns = content[i + 1].split(',') line_type = next_line_columns[3] current_test.append(line) @@ -206,7 +214,6 @@ def preparse_content(raw_content): return list(content.values()) - def dict_from_raw_station_test(content): """ Expects a string content with the station test output @@ -222,8 +229,7 @@ def dict_from_raw_station_test(content): row_type = row[3] if row_type == "STATION": station_name = row[4]['name'] - station_type = station_name[0].capitalize() - station_type = station_type if station_type in ['C', 'R'] else 'I' + station_type = station_type_from_station_name(station_name) result.update(station=dict(name=station_name, type=station_type)) elif row_type == 'VERSIONS': pass diff --git a/LCU/Maintenance/DBInterface/monitoringdb/tasks/generate_plots.py b/LCU/Maintenance/DBInterface/monitoringdb/tasks/generate_plots.py index 31abe3ae27f60def3723ad7651cdd7f79e552193..f3b77f45522a247176da100bab8b5aaff7674198 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/tasks/generate_plots.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/tasks/generate_plots.py @@ -1,10 +1,13 @@ import logging import os +try: -import matplotlib + import matplotlib -matplotlib.use('agg') -import matplotlib.pyplot as plt + matplotlib.use('agg') + import matplotlib.pyplot as plt +except ImportError: + pass import numpy from celery import shared_task from django.conf import settings @@ -110,7 +113,9 @@ def render_summary_text(error_metadata, observation_metadata): def produce_plot(observation_metadata, error_metadata, - list_bad_spectra, list_good_specta, path): + start_frequency, + stop_frequency, + list_bad_spectra, list_good_spectra, path): """ Produces a plot given the observation metadata the error summary metadata and the list of the bad spectra for the given (error, rcu) and the average spectrum @@ -118,7 +123,7 @@ def produce_plot(observation_metadata, :param observation_metadata: the metadata information regarding the observation :param error_metadata: the metadata information regarding the error :param list_bad_spectra: a list containing the bad spectra for the given error, rcu couple - :param list_good_specta: the average spectrum of the rest of the array + :param list_good_spectra: the average spectrum of the rest of the array :param path: the path where to store the file """ plt.figure(figsize=(12, 9)) @@ -127,18 +132,18 @@ def produce_plot(observation_metadata, plt.xlabel('MHz') plt.ylabel('dB') - frequency_sampling = len(list_bad_spectra[0][0]) + frequency_sampling = len(list_bad_spectra[0]) n_spectra = len(list_bad_spectra) - frequency = numpy.linspace(error_metadata.start_frequency, - error_metadata.stop_frequency, + frequency = numpy.linspace(start_frequency, + stop_frequency, frequency_sampling) bad_spectra_cube = numpy.zeros((n_spectra, frequency_sampling)) average_spectra_cube = numpy.zeros((n_spectra, frequency_sampling)) for i, spectrum in enumerate(list_bad_spectra): - bad_spectra_cube[i, :] = spectrum[0] - for i, spectrum in enumerate(list_good_specta): - average_spectra_cube[i, :] = spectrum[0] + bad_spectra_cube[i, :] = spectrum + for i, spectrum in enumerate(list_good_spectra): + average_spectra_cube[i, :] = spectrum min_bad_spectrum = numpy.min(bad_spectra_cube, axis=0) max_bad_spectrum = numpy.max(bad_spectra_cube, axis=0) @@ -191,7 +196,7 @@ def generate_summary_plot_for_error(error_summary_id): try: observation_metadata = ObservationMetadata() observation = error_summary.observation - observation_metadata.observation_id = observation.observation_id + observation_metadata.observation_id = observation.sas_id observation_metadata.station_name = observation.station.name observation_metadata.set_start_time(observation.start_datetime) observation_metadata.set_end_time(observation.end_datetime) @@ -201,8 +206,13 @@ def generate_summary_plot_for_error(error_summary_id): rcu=error_summary.rcu, error_type=error_summary.error_type) - list_bad_spectra = errors.values_list('spectra__bad_spectrum') - list_good_spectra = errors.values_list('spectra__average_spectrum') + list_bad_spectra, list_average_spectra = zip(*errors.values_list('samples__bad_spectrum', + 'samples__average_spectrum')) + + (start_frequency, *_), (stop_frequency, *_) = \ + zip(*errors.values_list('samples__start_frequency', + 'samples__stop_frequency').distinct()) + file_name = '%(observation_id)d_%(station_name)s_rcu%(rcu)d_%(error_type)s.png' % dict( observation_id=observation_metadata.observation_id, station_name=observation_metadata.station_name, @@ -211,10 +221,13 @@ def generate_summary_plot_for_error(error_summary_id): ) full_path = os.path.join(basePath, file_name) - produce_plot(observation_metadata, - error_summary, - list_bad_spectra, list_good_spectra, - full_path) + produce_plot(observation_metadata=observation_metadata, + error_metadata=error_summary, + start_frequency=start_frequency, + stop_frequency=stop_frequency, + list_bad_spectra=list_bad_spectra, + list_good_spectra=list_average_spectra, + path=full_path) summary_plot.uri = file_name summary_plot.save() return summary_plot.pk @@ -234,7 +247,7 @@ def check_error_summary_plot(error_summary_id): :return: """ logger.debug('looking for rtsm error summary %s', error_summary_id) - error_summary = RTSMErrorSummary.objects.get(pk=error_summary_id) + error_summary = RTSMError.objects.get(pk=error_summary_id) summary_plot = error_summary.summary_plot.first() logger.debug('summary error found %s', summary_plot) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/tasks/insert_raw_tests.py b/LCU/Maintenance/DBInterface/monitoringdb/tasks/insert_raw_tests.py index 92f1f48a0519f0299f766334846ade31e79c0751..7cdf93672d6c0c8984623d755bbb4490b162a8b3 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/tasks/insert_raw_tests.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/tasks/insert_raw_tests.py @@ -1,18 +1,38 @@ import logging +from typing import Dict from celery import shared_task from django.db import transaction + +# Station Models from lofar.maintenance.monitoringdb.models.component import Component -from lofar.maintenance.monitoringdb.models.component_error import ComponentError -from lofar.maintenance.monitoringdb.models.element import Element -from lofar.maintenance.monitoringdb.models.element_error import ElementError from lofar.maintenance.monitoringdb.models.station import Station -from lofar.maintenance.monitoringdb.models.station_test import StationTest -from lofar.maintenance.monitoringdb.station_test_raw_parser import parse_raw_station_test +from lofar.maintenance.monitoringdb.models.element import Element from lofar.maintenance.monitoringdb.models.log import ActionLog +from lofar.maintenance.monitoringdb.models.signals.generic_test_insert import on_inserting_test + +# RTSM Models +from lofar.maintenance.monitoringdb.models.rtsm import RTSMObservation, RTSMError, RTSMErrorSample + +# Station Test Models +from lofar.maintenance.monitoringdb.models.station_test import StationTest +from lofar.maintenance.monitoringdb.models.component_error import ComponentError +from lofar.maintenance.monitoringdb.models.element_error import ElementError + +from lofar.maintenance.monitoringdb.rtsm_test_raw_parser import parse_rtsm_test +from lofar.maintenance.monitoringdb.station_test_raw_parser import parse_raw_station_test, station_type_from_station_name + logger = logging.getLogger(__name__) + + +from django.db.models.signals import post_save + +post_save.connect(on_inserting_test, sender=RTSMObservation) +post_save.connect(on_inserting_test, sender=StationTest) + + def create_component_errors(station: Station, station_test: StationTest, component_errors_data): component_list = [] component_error_list = [] @@ -22,8 +42,8 @@ def create_component_errors(station: Station, station_test: StationTest, compone component_list.append(component_entity) component_error_entity, _ = ComponentError.objects.get_or_create(component=component_entity, - station_test=station_test, - **component_error) + station_test=station_test, + **component_error) component_error_list.append(component_error_entity) @@ -51,7 +71,11 @@ def create_element_errors(station, station_test, elements_error_data): def create_station(station_data): - station, created = Station.objects.get_or_create(**station_data) + + station, created = Station.objects.update_or_create(defaults=station_data, + name=station_data['name']) + + return station @@ -70,7 +94,6 @@ def create_station_tests(station_tests_data): element_errors = station_test_data.pop('element_errors') station = create_station(station_data) - station, created = Station.objects.get_or_create(station) station_test = create_station_test(station, station_test_data) @@ -80,6 +103,52 @@ def create_station_tests(station_tests_data): create_element_errors(station, station_test, element_errors) +def create_rtsm_error(observation:RTSMObservation, rtsm_error): + start_frequency = rtsm_error.pop('start_frequency') + stop_frequency = rtsm_error.pop('stop_frequency') + n_samples = observation.samples + samples = rtsm_error.pop('samples') + + count = rtsm_error.pop('count') + percentage = count / float(n_samples) + rtsm_error, created = RTSMError.get_or_create_error_from_rtsm_result(observation=observation, + count=count, + percentage=percentage, + **rtsm_error) + + if created: + rtsm_error_samples = [RTSMErrorSample(error=rtsm_error, + start_frequency=start_frequency, + stop_frequency=stop_frequency, + **error_sample) for error_sample in samples] + RTSMErrorSample.objects.bulk_create(rtsm_error_samples) + else: + for error_sample in samples: + RTSMErrorSample.objects.get_or_create(error=rtsm_error, + start_frequency=start_frequency, + stop_frequency=stop_frequency, + **error_sample) + + return rtsm_error + + +@transaction.atomic +def create_rtsm_test(rtsm_observation) -> RTSMObservation: + errors = rtsm_observation.pop('errors') + + station_name = rtsm_observation.pop('station_name') + sas_id = rtsm_observation.pop('observation_id') + station, _ = create_station(dict(name=station_name, + type=station_type_from_station_name(station_name))) + + rtsm_observation, _ = RTSMObservation.objects.get_or_create(station=station, + sas_id=sas_id, + **rtsm_observation) + for error in errors: + create_rtsm_error(rtsm_observation, error) + return rtsm_observation + + @shared_task() def insert_station_test(action_log_id: ActionLog, raw_tests): action_log = ActionLog.objects.get(pk=action_log_id) @@ -95,4 +164,32 @@ def insert_station_test(action_log_id: ActionLog, raw_tests): except Exception as e: action_log.what = 'INSERT_FAILED' action_log.save() - raise e \ No newline at end of file + raise e + + +@shared_task() +def insert_rtsm_test(action_log_id: ActionLog, request_data: Dict): + action_log = ActionLog.objects.get(pk=action_log_id) + try: + #logger.debug('handling raw request_data for %s', request_data) + content = request_data['content'] + + station_name = request_data['station_name'] + #logger.debug('handling raw request_data for data %s', content) + + entry = parse_rtsm_test(content) + + entry.update(station_name=station_name) + + rtsm_test = create_rtsm_test(entry) + + action_log.what = 'INSERTED' + action_log.save() + logger.info('RTSM parsed successfully for obsid %d and station %s', + rtsm_test.sas_id, + rtsm_test.station.name) + + except Exception as e: + action_log.what = 'INSERT_FAILED' + action_log.save() + raise e diff --git a/LCU/Maintenance/DBInterface/monitoringdb/urls.py b/LCU/Maintenance/DBInterface/monitoringdb/urls.py index d2a37ed245fd00ae3d96307b5a84efb0b7d3085d..16d6c3520eef0a29d8aa1c79c5b524211b2aff4f 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/urls.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/urls.py @@ -31,7 +31,8 @@ station_test_router.register(r'', StationTestViewSet) rtsm_router = routers.DefaultRouter() rtsm_router.register(r'errors', RTSMErrorsViewSet) -rtsm_router.register(r'spectra', RTSMSpectrumViewSet) +rtsm_router.register(r'error_sample', RTSMErrorSampleViewSet) + rtsm_router.register(r'error_summary_plot', RTSMSummaryPlot, base_name='rtsm-summary-plot') rtsm_router.register(r'', RTSMObservationViewSet) diff --git a/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py b/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py index 8d91dc0d5b1572f87c2054682974d3903f6e9de3..e182e42303852678dec3394d2ede8470e877c892 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py @@ -1,28 +1,31 @@ import datetime import logging from collections import OrderedDict +from collections import defaultdict from math import ceil +from django.utils import timezone import coreapi import coreschema import pytz from django.db.models import Count -from lofar.maintenance.monitoringdb.models.component_error import ComponentError -from lofar.maintenance.monitoringdb.models.rtsm import RTSMError, MODE_TO_COMPONENT, \ - COMPONENT_TO_MODE -from lofar.maintenance.monitoringdb.models.rtsm import RTSMObservation -from lofar.maintenance.monitoringdb.models.station import Station -from lofar.maintenance.monitoringdb.models.station_test import StationTest -from lofar.maintenance.monitoringdb.models.wincc import WinCCAntennaStatus,\ - latest_status_per_station_and_component_type,\ - latest_status_per_station_and_component_type_antenna_id from rest_framework import status from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import ManualSchema from rest_framework.views import APIView +from lofar.maintenance.monitoringdb.models.component_error import ComponentError +from lofar.maintenance.monitoringdb.models.rtsm import RTSMError +from lofar.maintenance.monitoringdb.models.rtsm import RTSMObservation +from lofar.maintenance.monitoringdb.models.station import Station +from lofar.maintenance.monitoringdb.models.station_test import StationTest +from lofar.maintenance.monitoringdb.models.wincc import WinCCAntennaStatus, \ + latest_status_per_station_and_component_type, \ + latest_status_per_station_and_component_type_antenna_id + logger = logging.getLogger(__name__) +from lofar.maintenance.monitoringdb.models.rtsm import MODE_TO_COMPONENT def parse_date(date): @@ -97,7 +100,6 @@ def antenna_id_polarization_from_rcu_type_polarization(rcu, type): :param type: type of the antenna :return: the antenna id and polarization :rtype: (int, str) - :rtype: int """ polarization_index = rcu % 2 @@ -188,6 +190,31 @@ class ValidableReadOnlyView(APIView): return response +def compute_error_summary(station_test:StationTest, selected_error_types=None): + component_error_summary = dict() + + for component_error in station_test.component_errors.all(): + component_type = component_error.component.type + component_error_type = component_error.type + + if selected_error_types and component_error_type not in selected_error_types: + continue + + if component_type not in component_error_summary: + component_error_summary[component_type] = {component_error_type: 1} + elif component_error_type not in component_error_summary[component_type]: + component_error_summary[component_type][component_error_type] = 1 + else: + component_error_summary[component_type][component_error_type] += 1 + + return component_error_summary + +from django.db.models import Window, F +from django.db.models.functions import Rank + +from silk.profiling.profiler import silk_profile + + class ControllerStationOverview(ValidableReadOnlyView): description = "Overview of the latest tests performed on the stations" @@ -241,91 +268,101 @@ class ControllerStationOverview(ValidableReadOnlyView): ) ] - def compute_response(self): - station_entities = Station.objects.all() - for group in self.station_group: - if group is not 'A': - station_entities = station_entities.filter(type=group) + def get_last_station_test_per_station(self, selected_stations): + expected_tests = len(selected_stations) * self.n_station_tests - # Since django preferes a ordered dict over a dict we make it happy... for now - response_payload = list() - for station_entity in station_entities: - station_payload = OrderedDict() + station_test_instances = StationTest.objects.filter(station__name__in=selected_stations). \ + annotate(order=Window( + expression=Rank(), + partition_by=[F('station')], + order_by=F('start_datetime').desc()) + ).order_by('order')[:expected_tests].\ + select_related('station').prefetch_related('component_errors', + 'component_errors__component') - station_payload['station_name'] = station_entity.name + st_per_station = defaultdict(list) + for station_test in station_test_instances: + station_name = station_test.station.name - station_test_list = StationTest.objects.filter( - station__name=station_entity.name).order_by('-end_datetime')[:self.n_station_tests] - rtsm_list = RTSMObservation.objects.filter( - station__name=station_entity.name).order_by('-end_datetime')[:self.n_rtsm] + test_summary = dict() + test_summary.update(start_datetime=station_test.start_datetime) + test_summary.update(end_datetime=station_test.end_datetime) + test_summary.update(checks=station_test.checks) - station_payload['station_tests'] = list() - for station_test in station_test_list: - station_test_payload = OrderedDict() - component_errors = station_test.component_errors - selected_component_errors = component_errors - if self.error_types: - selected_component_errors = component_errors.filter(type__in=self.error_types) - station_test_payload[ - 'total_component_errors'] = selected_component_errors.count() - station_test_payload['start_datetime'] = station_test.start_datetime - station_test_payload['end_datetime'] = station_test.end_datetime - station_test_payload['checks'] = station_test.checks - - component_errors_summary = selected_component_errors. \ - values('component__type', 'type').annotate( - total=Count('type')).order_by('-total') - component_errors_summary_dict = OrderedDict() - for item in component_errors_summary: - item_component_type = item['component__type'] - item_error_type = item['type'] - item_error_total = item['total'] - - if item_component_type not in component_errors_summary_dict: - component_errors_summary_dict[item_component_type] = OrderedDict() - - component_errors_summary_dict[item_component_type][item_error_type] = \ - item_error_total - station_test_payload['component_error_summary'] = component_errors_summary_dict - - station_payload['station_tests'].append(station_test_payload) - - station_payload['rtsm'] = list() - for rtsm in rtsm_list: - rtsm_payload = OrderedDict() - rtsm_payload['observation_id'] = rtsm.observation_id - rtsm_payload['start_datetime'] = rtsm.start_datetime - rtsm_payload['end_datetime'] = rtsm.end_datetime - - unique_modes = [item['mode'] for item in - rtsm.errors_summary.values('mode').distinct()] - rtsm_payload['mode'] = unique_modes - - selected_rtsm_errors = rtsm.errors_summary - if self.error_types: - selected_rtsm_errors = rtsm.errors_summary.filter( - error_type__in=self.error_types) + component_error_summary = compute_error_summary(station_test, self.error_types) - rtsm_payload['total_component_errors'] = selected_rtsm_errors.count() + test_summary.update(component_error_summary=component_error_summary) - errors_summary = OrderedDict() + st_per_station[station_name] = test_summary + return st_per_station - errors_summary_query = selected_rtsm_errors.values('error_type').annotate( - total=Count('error_type')) + def get_last_rtsm(self, selected_stations): - for error_summary in errors_summary_query: - errors_summary[error_summary['error_type']] = error_summary['total'] + expected_tests = len(selected_stations) * self.n_rtsm - rtsm_payload['error_summary'] = errors_summary - station_payload['rtsm'].append(rtsm_payload) + rtsm_instances = RTSMObservation.objects.filter(station__name__in=selected_stations). \ + annotate(order=Window( + expression=Rank(), + partition_by=[F('station')], + order_by=F('start_datetime').desc()) + ).order_by('order')[:expected_tests].prefetch_related('errors', 'station') + + rtsm_per_station = defaultdict(list) + for rtsm in rtsm_instances: + + station_name = rtsm.station.name + observation_id = rtsm.sas_id + start_datetime = rtsm.start_datetime + end_datetime = rtsm.end_datetime + + rtsm_summary = dict() + rtsm_summary.update(observation_id=observation_id, + start_datetime=start_datetime, + end_datetime=end_datetime) + error_summary = dict() + + count = 0 + for error in rtsm.errors.all(): + if self.error_types and error not in self.error_types: + continue + + if error.error_type not in error_summary: + error_summary[error.error_type] = 1 + count = 1 + + error_summary[error.error_type] += 1 + count += 1 + + rtsm_summary.update(total_component_errors=count, + error_summary=error_summary) + + rtsm_per_station[station_name].append(rtsm_summary) + + return rtsm_per_station + + @silk_profile(name='Computing response') + def compute_response(self): + station_entities = Station.objects.all() + + if 'A' not in self.station_group: + station_entities = station_entities.filter(type__in=self.station_group)\ + + station_entities = list(zip(*station_entities.values_list('name')))[0] + + + # Since django preferes a ordered dict over a dict we make it happy... for now + rtsm_per_station = self.get_last_rtsm(station_entities) + st_per_station = self.get_last_station_test_per_station(station_entities) + response_payload = [ + dict( + station_name=station, + rtsm=rtsm_per_station[station], + station_tests=st_per_station[station] + ) + + for station in station_entities + ] - response_payload.append(station_payload) - if self.errors_only: - response_payload = filter( - lambda station_entry: - len(station_entry['station_tests']) + len(station_entry['rtsm']) > 0, - response_payload) - response_payload = sorted(response_payload, key=lambda item: item['station_name']) return Response(status=status.HTTP_200_OK, data=response_payload) @@ -372,51 +409,36 @@ class ControllerStationTestsSummary(ValidableReadOnlyView): ) ] + @silk_profile(name='Computing response') def compute_response(self): - self.lookback_time = datetime.timedelta(days=self.lookback_time) - station_test_list = StationTest.objects \ - .filter(start_datetime__gte=datetime.date.today() - self.lookback_time) \ - .order_by('-start_datetime', 'station__name') - for group in self.station_group: - if group is not 'A': - station_test_list = station_test_list.filter(station__type=group) + from_date = timezone.now() - timezone.timedelta(days=self.lookback_time) + + station_test_entities = StationTest.objects.filter(start_datetime__gt=from_date).\ + select_related('station') + + if 'A' not in self.station_group: + station_test_entities = station_test_entities.filter(station__type__in=self.station_group) + + + station_test_entities = \ + station_test_entities.prefetch_related('component_errors', + 'component_errors__component').order_by('-start_datetime') # Since django preferes a ordered dict over a dict we make it happy... for now response_payload = list() - for station_test in station_test_list: - - station_test_payload = OrderedDict() - station_test_payload['station_name'] = station_test.station.name - selected_component_errors = station_test.component_errors - if (self.error_types): - selected_component_errors = selected_component_errors.filter( - type__in=self.error_types) - - station_test_payload[ - 'total_component_errors'] = selected_component_errors.count() - station_test_payload['date'] = station_test.start_datetime.strftime('%Y-%m-%d') - station_test_payload['start_datetime'] = station_test.start_datetime - station_test_payload['end_datetime'] = station_test.end_datetime - station_test_payload['checks'] = station_test.checks - component_errors_summary = selected_component_errors. \ - values('component__type', 'type').annotate( - total=Count('type')).order_by('-total') - component_errors_summary_dict = OrderedDict() - for item in component_errors_summary: - item_component_type = item['component__type'] - item_error_type = item['type'] - item_error_total = item['total'] - - if item_component_type not in component_errors_summary_dict: - component_errors_summary_dict[item_component_type] = OrderedDict() - - component_errors_summary_dict[item_component_type][item_error_type] = \ - item_error_total - station_test_payload['component_error_summary'] = component_errors_summary_dict - - response_payload.append(station_test_payload) + for station_test in station_test_entities: + station_test_summary = dict() + station_test_summary.update(station_name=station_test.station.name) + station_test_summary.update(total_component_errors=station_test.component_errors.count()) + station_test_summary.update(start_datetime=station_test.start_datetime) + station_test_summary.update(end_datetime=station_test.end_datetime) + station_test_summary.update(date=station_test.start_datetime.strftime("%Y-%m-%d")) + + component_error_summary = compute_error_summary(station_test, self.error_types) + station_test_summary.update(component_error_summary=component_error_summary) + response_payload.append(station_test_summary) if self.errors_only: response_payload = filter( @@ -795,65 +817,59 @@ class ControllerStationComponentErrors(ValidableReadOnlyView): ] def collect_station_test_errors(self): - station_entry = Station.objects.filter(name=self.station_name).first() + + component_errors = ComponentError.objects.filter(station_test__station__name=self.station_name).\ + filter(station_test__start_datetime__range=(self.from_date, self.to_date)).\ + select_related('station_test', 'component').\ + prefetch_related('station_test__station','station_test__station_status').order_by('-station_test__start_datetime', 'component__station__name') response_payload = OrderedDict() - station_tests = station_entry.stationtest_set \ - .filter(start_datetime__gte=self.from_date, - end_datetime__lte=self.to_date) + for component_error in component_errors: + component_id = component_error.component.component_id + component_type = component_error.component.type + test_id = component_error.station_test.pk - failing_component_types = station_tests.distinct('component_errors__component__type'). \ - exclude(component_errors__component__type__isnull=True). \ - values_list('component_errors__component__type') + component_error_summary = dict(error_type=component_error.type, + details=component_error.details) - for failing_component_type in failing_component_types: - failing_component_type = failing_component_type[0] - component_type_errors_list = list() + if component_type not in response_payload: + response_payload[component_type] = dict() - response_payload[failing_component_type] = component_type_errors_list + per_component_type = response_payload[component_type] - for station_test in station_tests.order_by('-start_datetime'): - test_summary = OrderedDict() - test_summary['test_type'] = 'S' - test_summary['start_date'] = station_test.start_datetime - test_summary['end_date'] = station_test.end_datetime - component_errors_dict = OrderedDict() - antenna_statuses = latest_status_per_station_and_component_type( - WinCCAntennaStatus.objects, - self.station_name, - failing_component_type, - to_date=station_test.end_datetime) + if test_id not in per_component_type: - test_summary['status'] = antenna_statuses - test_summary['component_errors'] = component_errors_dict - component_errors = station_test.component_errors \ - .filter(component__type=failing_component_type) - if self.error_types: - component_errors = component_errors.filter(type__in=self.error_types) + per_component_type[test_id] = dict( + start_datetime = component_error.station_test.start_datetime, + end_datetime = component_error.station_test.end_datetime, + test_type = 'S', + status=component_error.station_test.station_status, + component_errors=dict() + ) - for component_error in component_errors: - component_id = component_error.component.component_id - error_type = component_error.type - details = component_error.details - if component_id not in component_errors: - component_errors_dict[str(component_id)] = list() - component_errors_dict[str(component_id)] += [dict(error_type=error_type, - details=details)] + per_test = per_component_type[test_id]['component_errors'] + + if component_id not in per_test: + per_test[component_id] = [component_error_summary] + else: + per_test[component_id] += [component_error_summary] - component_type_errors_list.append(test_summary) + + for type in response_payload: + response_payload[type] = list(response_payload[type].values()) return response_payload def collect_rtsm_errors(self): station_entry = Station.objects.filter(name=self.station_name).first() response_payload = OrderedDict() - rtsm_observations = station_entry.rtsmobservation_set \ + rtsm_observations = RTSMObservation.objects.filter(station=station_entry) \ .filter(start_datetime__gte=self.from_date, end_datetime__lte=self.to_date) - failing_component_modes = rtsm_observations.exclude(errors_summary__isnull=True).distinct( - 'errors_summary__mode').values_list('errors_summary__mode') + failing_component_modes = rtsm_observations.exclude(errors__isnull=True).distinct( + 'errors__mode').values_list('errors__mode') for observing_mode in failing_component_modes: observing_mode = observing_mode[0] @@ -867,7 +883,7 @@ class ControllerStationComponentErrors(ValidableReadOnlyView): rtsm_summary['test_type'] = 'R' rtsm_summary['start_date'] = rtsm_observation.start_datetime rtsm_summary['end_date'] = rtsm_observation.end_datetime - rtsm_summary['observation_id'] = rtsm_observation.observation_id + rtsm_summary['observation_id'] = rtsm_observation.sas_id antenna_statuses = latest_status_per_station_and_component_type( WinCCAntennaStatus.objects, self.station_name, @@ -877,7 +893,7 @@ class ControllerStationComponentErrors(ValidableReadOnlyView): component_errors_dict = OrderedDict() rtsm_summary['component_errors'] = component_errors_dict - component_errors = rtsm_observation.errors_summary \ + component_errors = rtsm_observation.errors \ .filter(mode=observing_mode) \ .values('error_type', 'start_frequency', 'stop_frequency', 'percentage', @@ -938,22 +954,23 @@ class ControllerStationComponentErrors(ValidableReadOnlyView): station_test_errors = self.collect_station_test_errors() if self.test_type in ['R', 'B']: - rtsm_errors = self.collect_rtsm_errors() + rtsm_errors = {} # self.collect_rtsm_errors() payload = OrderedDict() for component_type in set(rtsm_errors.keys() | station_test_errors.keys()): station_test_errors_per_type = station_test_errors.get(component_type, []) rtsm_errors_per_type = rtsm_errors.get(component_type, []) payload[component_type] = OrderedDict() - - payload[component_type]['errors'] = sorted(station_test_errors_per_type + rtsm_errors_per_type, - key=lambda item: item['start_date'], reverse=True) - - payload[component_type]['current_status'] =\ - latest_status_per_station_and_component_type(WinCCAntennaStatus.objects, - self.station_name, - component_type, - to_date=datetime.datetime.now()) + payload[component_type]['errors'] = station_test_errors_per_type + #payload[component_type]['errors'] = sorted( + # station_test_errors_per_type + rtsm_errors_per_type, + # key=lambda item: item['start_date'], reverse=True) + + #payload[component_type]['current_status'] = \ + # latest_status_per_station_and_component_type(WinCCAntennaStatus.objects, + # self.station_name, + # component_type, + # to_date=datetime.datetime.now()) return Response(status=status.HTTP_200_OK, data=payload) @@ -1079,17 +1096,17 @@ class ControllerStationComponentElementErrors(ValidableReadOnlyView): self.component_type) rtsm_errors = RTSMError.objects.values('observation__pk', - 'observation__start_datetime', - 'observation__end_datetime', - 'observation__observation_id', - 'observation__station__name', - 'pk', - 'rcu', - 'mode', - 'error_type', - 'percentage', - 'count', - 'observation__samples').filter( + 'observation__start_datetime', + 'observation__end_datetime', + 'observation__observation_id', + 'observation__station__name', + 'pk', + 'rcu', + 'mode', + 'error_type', + 'percentage', + 'count', + 'observation__samples').filter( observation__start_datetime__gt=self.from_date, observation__end_datetime__lt=self.to_date, observation__station__name=self.station_name, @@ -1204,4 +1221,3 @@ class ControllerStationComponentElementErrors(ValidableReadOnlyView): self.antenna_id, to_date=datetime.datetime.now()) return Response(status=status.HTTP_200_OK, data=payload) - diff --git a/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py b/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py index d41760307a5bc5c9ae1f2b64272017e16b4dcff9..1a7a78e3220c959c27e440ab06f6cae20f325803 100644 --- a/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py +++ b/LCU/Maintenance/DBInterface/monitoringdb/views/rtsm_views.py @@ -1,22 +1,18 @@ from .common import * -from ..models.rtsm import RTSMObservation, RTSMError, RTSMSpectrum -from ..serializers.rtsm import RTSMObservationSerializer, RTSMErrorSerializer, \ - RTSMSpectrumSerializer, RTSMSummaryPlotSerializer, RTSMError -from ..rtsm_test_raw_parser import parse_rtsm_test +from ..models.rtsm import RTSMObservation +from ..models.log import ActionLog +from ..serializers.rtsm import RTSMObservationSerializer, RTSMErrorSerializer,\ + RTSMSummaryPlotSerializer, RTSMError, RTSMErrorSample, RTSMErrorSampleSerializer +from datetime import datetime +from lofar.maintenance.monitoringdb.tasks.insert_raw_tests import insert_rtsm_test from django.shortcuts import get_object_or_404 -from django.http import Http404, HttpResponseServerError +from django.http import Http404 import os from lofar.maintenance.monitoringdb.tasks.generate_plots import check_error_summary_plot logger = logging.getLogger('views') -class RTSMSpectrumViewSet(viewsets.ReadOnlyModelViewSet): - queryset = RTSMSpectrum.objects.all() - serializer_class = RTSMSpectrumSerializer - filter_fields = ['rtsm_error'] - - class RTSMErrorsViewSet(viewsets.ReadOnlyModelViewSet): queryset = RTSMError.objects.all() serializer_class = RTSMErrorSerializer @@ -29,6 +25,12 @@ class RTSMObservationViewSet(viewsets.ReadOnlyModelViewSet): filter_fields = '__all__' +class RTSMErrorSampleViewSet(viewsets.ReadOnlyModelViewSet): + queryset = RTSMErrorSample.objects.all() + serializer_class = RTSMErrorSampleSerializer + exclude_fields = 'bad_spectrum', 'average_spectrum' + + class RTSMSummaryPlot(viewsets.ViewSet): """ Get the summary plot associated to the error summary given @@ -74,37 +76,28 @@ def insert_raw_rtsm_test(request): if 'content' in request.data: try: - logger.debug('handling raw request for %s', request) - content = request.data['content'] - station_name = request.data['station_name'] - logger.debug('handling raw request for data %s', content) - entry = parse_rtsm_test(content) - entry.update(station_name=station_name) - - logger.info('RTSM parsed successfully for obsid %d and station %s', - entry['observation_id'], - entry['station_name']) - - try: - rtsm_item = RTSMObservationSerializer().create(dict(entry)) - rtsm_id = rtsm_item.id - except ItemAlreadyExists as e: - return Response(status=status.HTTP_200_OK, - data='RTSM test was already present with id {}\n'.format(e.instance_id)) - logger.info('RTSM inserted successfully for obsid %d and station %s', - entry['observation_id'], - entry['station_name']) - except KeyError as e: - logger.exception("raw station info malformed %s: %s", - request.data['content'], e) - logger.info('station test malformed skipping insertion.') - return Response(status=status.HTTP_406_NOT_ACCEPTABLE, data='skipping insertion RTSM malformed') + logger.info('handling raw request for %s', request) + + logger.info('handling raw request for data %s', request.data) + + action_log = ActionLog(who=request.META['REMOTE_ADDR'], + what='INSERT_REQUESTED', + when=datetime.now(), + model_name='StationTest', + entry_id=-1) + + action_log.save() + + insert_rtsm_test.delay(action_log.pk, request.data) + + return Response(status=status.HTTP_200_OK, + data='RTSM tests insert acknowledged.' + ' Check log for id %s' % action_log.pk) except Exception as e: - logger.exception("exception occurred while parsing raw station info %s: %s", - request.data['content'], e) + logger.exception("exception occurred while parsing raw station info: %s", e) return Response(exception=True, data="the post message is not correct." + - " It has to be of the form {content:[RAWSTRING], station_name:[STATION_NAME]}: %s. Request provided %s" % ( + " It has to be of the form \{'content':[RAWSTRING]\}: %s. Request provided %s" % ( e, request), status=status.HTTP_400_BAD_REQUEST) else: @@ -112,7 +105,3 @@ def insert_raw_rtsm_test(request): data="the post message is not correct." + " It has to be of the form \{'content':[RAWSTRING]\}", status=status.HTTP_400_BAD_REQUEST) - logger.info('request processed correctly inserted') - - return Response(status=status.HTTP_200_OK, - data='RTSM test has been inserted with id {}\n'.format(rtsm_id)) diff --git a/LCU/Maintenance/MDB_tools/lib/client.py b/LCU/Maintenance/MDB_tools/lib/client.py index 64036c4cfb3d9fd8ef4ba23401adbe2cba97ff7b..b84538036130a1e1bab354cf0c031efdc1958d78 100644 --- a/LCU/Maintenance/MDB_tools/lib/client.py +++ b/LCU/Maintenance/MDB_tools/lib/client.py @@ -64,10 +64,14 @@ def is_station_test(content): :return: True if the content refers to a RTSM and it is not empty """ if content: - pattern = r'(^\d*,[A-z]{3},[\d-]{3})' + pattern = r'(^#.*)|(^\d*,[A-z]{3},[\d-]{3})' number_of_lines = len(content.splitlines()) pattern_matching_lines = re.findall(pattern, content, re.MULTILINE) number_pattern_matching_lines = len(pattern_matching_lines) + logger.debug('Station Test: number of matched lines %d/%d number of lines in files', + number_pattern_matching_lines, + number_of_lines) + return number_of_lines == number_pattern_matching_lines else: return False @@ -97,6 +101,9 @@ OBS-ID-INFO=641999,1520208240.000,1520211840.000,59 number_of_lines = len(content.splitlines()) pattern_matching_lines = re.findall(pattern, content, re.MULTILINE) number_pattern_matching_lines = len(pattern_matching_lines) + logger.debug('RTSM: number of matched lines %d/%d number of lines in files', + number_pattern_matching_lines, + number_of_lines) return number_of_lines == number_pattern_matching_lines else: return False diff --git a/MAC/CMakeLists.txt b/MAC/CMakeLists.txt index 6ad267da65bc4ae5f951af579c5a00d4b3fbbd0d..c1c9d2dcfb965a6dbdd6572cc3300a12fdf420e8 100644 --- a/MAC/CMakeLists.txt +++ b/MAC/CMakeLists.txt @@ -11,6 +11,10 @@ lofar_add_package(WinCC_Datapoints Deployment/data/PVSS) lofar_add_package(OTDB_Comps Deployment/data/OTDB) lofar_add_package(StaticMetaData Deployment/data/StaticMetaData) lofar_add_package(WinCCPublisher WinCCPublisher) +lofar_add_package(WinCCREST) +lofar_add_package(WinCCDBBridge) + + lofar_add_package(TaskManagement Services/TaskManagement) lofar_add_package(TBB) diff --git a/MAC/WinCCDBBridge/CMakeLists.txt b/MAC/WinCCDBBridge/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..15920ec401f759d14377860b23d0b266c5050178 --- /dev/null +++ b/MAC/WinCCDBBridge/CMakeLists.txt @@ -0,0 +1,20 @@ +lofar_package(WinCCDBBridge 1.0 DEPENDS WinCCWrapper) + +include(LofarFindPackage) +lofar_find_package(WINCC REQUIRED) + +find_package(Qt5 COMPONENTS Core Network Sql Xml REQUIRED) + +lofar_find_package(UnitTest++) + +# Auto generate moc files +set(CMAKE_AUTOMOC ON) +# As moc files are generated in the binary dir, tell CMake +# to always look for includes there: +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +include_directories(${WinCCWrapper_SOURCE_DIR}/include/) + +add_subdirectory(etc) +add_subdirectory(src) +add_subdirectory(test) diff --git a/MAC/WinCCDBBridge/DB.ini b/MAC/WinCCDBBridge/DB.ini new file mode 100644 index 0000000000000000000000000000000000000000..6a9648414d6ec8a949120c7288454338a0c3af5a --- /dev/null +++ b/MAC/WinCCDBBridge/DB.ini @@ -0,0 +1,7 @@ +[Database] +type=QSQLITE +name=datapoints.db +address=localhost +port= +user= +pass= diff --git a/MAC/WinCCDBBridge/Docker/Dockerfile b/MAC/WinCCDBBridge/Docker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b0fefa428c181f6b113308438ff5d4bab0e7d926 --- /dev/null +++ b/MAC/WinCCDBBridge/Docker/Dockerfile @@ -0,0 +1,54 @@ +#DOCKER FILE TO BUILD THE WinCCDBBridge service that connects to WinCC to copy the datapoint value +#into any external sql database (using the QT libraries) +# THE WINCC RPM HAVE TO BE AVAILABLE IN THE DIRECTORY IN WHICH THE DOCKER IMAGE IS BUILT +# ./build/ +ARG SOURCES_ROOT=./ + +# THE BASE IMAGE STAGE +# it contains all the dependecies to run the application with WinCC +FROM centos:latest as base +WORKDIR /root/wincc_rpm +COPY ./build/*3.14*.rpm ./ +RUN yum -y install libicu +RUN yum -y install *3.14*.rpm +RUN yum -y install python boost qt5-qtbase +ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/WinCC_OA/3.14/bin/:/opt/WinCC_OA/3.14/api/lib.linux/ +RUN ldconfig +ENV PATH=$PATH:/opt/WinCC_OA/3.14/bin/ + +# THE BUILDER IMAGE STAGE +# In this stage all the development tools are installed +# the source code pulled from svn and the software built +# as it is necessary +FROM base as builder +RUN yum -y groupinstall "Development Tools" +RUN yum -y install cmake python-devel boost-devel qt5-qtbase-devel + +WORKDIR /root +COPY ${SOURCES_ROOT} ./ +RUN mkdir -p build/gnucxx11_debug/ +WORKDIR /root/build/gnucxx11_debug/ +ARG INSTALLDIR=/opt/lofar/ +RUN cmake ../../ -DBUILD_PACKAGES=WinCCDBBridge -DUSE_LOG4CPLUS=OFF -DWINCC_ROOT_DIR=/opt/WinCC_OA/3.14 -DCMAKE_INSTALL_PREFIX=$INSTALLDIR +ARG BUILD_PROCESSES=4 +RUN make -j $BUILD_PROCESSES && make install + +# DEPLOY IMAGE STAGE +# The image to deploy is built at this step +# all the develping dependecies are not useful anymore +# hence a image pruned with unnecessary dependecies is built +FROM base as deploy +COPY --from=builder /opt/lofar /opt/lofar +WORKDIR /opt/lofar +ARG WINCCPROJECT_DIR=$INSTALLDIR/var/wincc/LOFAR/ +ENV WINCCPROJ=LOFAR +ADD MAC/WinCCDBBridge/Docker/LOFAR.tar.gz $WINCCPROJECT_DIR +WORKDIR $WINCCPROJECT_DIR +RUN PVSSutil.sh RegisterProject 1.0 $WINCCPROJECT_DIR /opt/WinCC_OA/3.14/ +ADD MAC/WinCCDBBridge/Docker/run.sh /opt/lofar/bin/run.sh +ENV PATH=$PATH:/opt/lofar/bin +ENV QT_LOGGING_RULES="*.debug=true" +ENV QT_MESSAGE_PATTERN="%{time} - %{appname} %{pid}/%{threadid}: %{message}" +ENTRYPOINT ["/opt/lofar/bin/run.sh"] +CMD ["-h"] + diff --git a/MAC/WinCCDBBridge/Docker/LOFAR.tar.gz b/MAC/WinCCDBBridge/Docker/LOFAR.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..95d3deab96e174eeb9abbd88e72603722bf14fa5 Binary files /dev/null and b/MAC/WinCCDBBridge/Docker/LOFAR.tar.gz differ diff --git a/MAC/WinCCDBBridge/Docker/run.sh b/MAC/WinCCDBBridge/Docker/run.sh new file mode 100755 index 0000000000000000000000000000000000000000..9d2c32fccc5be63403c0f30c21cbc938c477a60c --- /dev/null +++ b/MAC/WinCCDBBridge/Docker/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +exec /opt/lofar/bin/winccbridge "$@" ${WINCCPROJ} diff --git a/MAC/WinCCDBBridge/etc/CMakeLists.txt b/MAC/WinCCDBBridge/etc/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MAC/WinCCDBBridge/etc/DB.ini b/MAC/WinCCDBBridge/etc/DB.ini new file mode 100644 index 0000000000000000000000000000000000000000..6a9648414d6ec8a949120c7288454338a0c3af5a --- /dev/null +++ b/MAC/WinCCDBBridge/etc/DB.ini @@ -0,0 +1,7 @@ +[Database] +type=QSQLITE +name=datapoints.db +address=localhost +port= +user= +pass= diff --git a/MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xml b/MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xml new file mode 100644 index 0000000000000000000000000000000000000000..37cecbbff1b05953ad94faf4ef4f39f394a51c68 --- /dev/null +++ b/MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xml @@ -0,0 +1,34 @@ +<Table name="antenna_statuses"> + <Datapoints> + <Datapoint name="System1:T1.level" alias="WaterLevel1"> + <description>Test datapoint in the GettingStarted example of WinCCOA</description> + <CheckpointInterval value="-3" /> + <index> + <regex> + <![CDATA[(?<system>\w*)\.((?<attribute>\w*))]]> + </regex> + <attribute name="system" alias="system" type="STRING"/> + <attribute name="attribute" alias="attribute" type="STRING"/> + </index> + <attributes> + <attribute name=":_original.._value" alias="value" type="STRING" unit="m" min="-0.3" max="0.3"/> + <attribute name=":_original.._stime" alias="lastChangeDate" type="STRING" unit=""/> + </attributes> + </Datapoint> + <Datapoint name="System1:T2.level" alias="WaterLevel2"> + <description>Test datapoint in the GettingStarted example of WinCCOA</description> + <CheckpointInterval value="-3" /> + <index> + <regex> + <![CDATA[(?<system>\w*)\.((?<attribute>\w*))]]> + </regex> + <attribute name="system" alias="system" type="STRING"/> + <attribute name="attribute" alias="attribute" type="STRING"/> + </index> + <attributes> + <attribute name=":_original.._value" alias="value" type="STRING" unit=""/> + <attribute name=":_original.._stime" alias="lastChangeDate" type="STRING" unit=""/> + </attributes> + </Datapoint> + </Datapoints> +</Table> diff --git a/MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xsd b/MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xsd new file mode 100644 index 0000000000000000000000000000000000000000..963fec103337ba583d938cdafb5be93fa338ef3c --- /dev/null +++ b/MAC/WinCCDBBridge/etc/Datapoints/antennaStatuses.xsd @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"> + <xs:element name="Table"> + <xs:complexType> + <xs:sequence> + <xs:element ref="Datapoints"/> + </xs:sequence> + <xs:attribute name="name" use="required" type="xs:NCName"/> + </xs:complexType> + </xs:element> + <xs:element name="Datapoints"> + <xs:complexType> + <xs:sequence> + <xs:element maxOccurs="unbounded" ref="Datapoint"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="Datapoint"> + <xs:complexType> + <xs:sequence> + <xs:element ref="description"/> + <xs:element ref="CheckpointInterval"/> + <xs:element ref="index"/> + <xs:element ref="attributes"/> + </xs:sequence> + <xs:attribute name="name" use="required" type="xs:NMTOKEN"/> + <xs:attribute name="alias" use="required" type="xs:NCName"/> + </xs:complexType> + </xs:element> + <xs:element name="description" type="xs:string"/> + <xs:element name="CheckpointInterval"> + <xs:complexType> + <xs:attribute name="value" use="required" type="xs:integer"/> + </xs:complexType> + </xs:element> + <xs:element name="index"> + <xs:complexType> + <xs:sequence> + <xs:element ref="regex"/> + <xs:element maxOccurs="unbounded" ref="attribute"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="regex" type="xs:string"/> + <xs:element name="attributes"> + <xs:complexType> + <xs:sequence> + <xs:element maxOccurs="unbounded" ref="attribute"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="attribute"> + <xs:complexType> + <xs:attribute name="name" use="required" type="xs:NMTOKEN"/> + <xs:attribute name="alias" use="required" type="xs:NCName"/> + <xs:attribute name="type" use="required" type="xs:NCName"/> + <xs:attribute name="max" type="xs:decimal"/> + <xs:attribute name="min" type="xs:decimal"/> + <xs:attribute name="unit" type="xs:string"/> + <xs:attribute name="value" type="xs:string"/> + </xs:complexType> + </xs:element> +</xs:schema> diff --git a/MAC/WinCCDBBridge/src/CMakeLists.txt b/MAC/WinCCDBBridge/src/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..e97fdf40375e2ee97c6f61ba35db0e396104783c --- /dev/null +++ b/MAC/WinCCDBBridge/src/CMakeLists.txt @@ -0,0 +1,25 @@ + +include(LofarFindPackage) +cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR) +file(GLOB_RECURSE WinCCDispatcher_SRCS *.cpp *.hpp) + + +include_directories(${WinCCWrapper_SOURCE_DIR}/include/) +include_directories( + ${CMAKE_SOURCE_DIR} + ${CMAKE_BINARY_DIR} + ) +include_directories(${Qt5_INCLUDE_DIRS}) + +# Create the application +lofar_add_bin_program(winccbridge ${WinCCDispatcher_SRCS}) + +target_link_libraries(winccbridge + Qt5::Core + Qt5::Network + Qt5::Sql + Qt5::Xml + wincc_wrapper +) + +target_compile_options(winccbridge PRIVATE -std=c++11) diff --git a/MAC/WinCCDBBridge/src/DatapointAttribute.cpp b/MAC/WinCCDBBridge/src/DatapointAttribute.cpp new file mode 100644 index 0000000000000000000000000000000000000000..44bae647d71914febf1d3c29c5097513e4712617 --- /dev/null +++ b/MAC/WinCCDBBridge/src/DatapointAttribute.cpp @@ -0,0 +1,180 @@ +#include "DatapointAttribute.hpp" +#include "DatapointRule.hpp" + +#include <limits> + +#include <QSqlField> +#include <QVariant> +#include <QSqlDatabase> +#include <QSqlDriver> +#include <QStringList> +#include <QDomElement> + +#include <QDebug> + +DatapointAttribute::DatapointAttribute(): + m_max{std::numeric_limits<double>::max()}, + m_min{std::numeric_limits<double>::min()}, + checkBoundaries{false} +{ + +} + +DatapointAttribute::DatapointAttribute(QString name, QString alias, QString type, QString unit, QString value, double minValue, double maxValue, bool check): + m_name{name}, m_alias{alias}, m_type{type}, m_unit{unit}, m_value{value}, m_max{maxValue}, m_min{minValue}, checkBoundaries{check} +{ + +} + +QString const DatapointAttribute::formatValue() const{ + if(m_type == "STRING"){ + + QSqlField field{m_alias, QVariant::String}; + field.setValue(m_value); + + QSqlDatabase db = QSqlDatabase::database(); + + if(db.isValid() == true) { + return db.driver()->formatValue(field); + }else{ + qWarning() << "Default database it is not configured"; + return "\'" + m_value + "\'"; + } + }else{ + return m_value; + } +} + + +QString DatapointAttribute::composeDatapointURI(DatapointRule &rule){ + return rule.name() + m_name; +} + +void DatapointAttribute::setParentRule(DatapointRule& rule) +{ + m_uri = composeDatapointURI(rule); +} + +const QString& DatapointAttribute::URI() const +{ + return m_uri; +} + +void DatapointAttribute::setURI(const QString& uri) +{ + m_uri = uri; +} + +const QString& DatapointAttribute::name() const +{ + return m_name; +} + +void DatapointAttribute::setName(const QString& name) +{ + m_name = name; +} + +const QString& DatapointAttribute::alias() const +{ + return m_alias; +} + +void DatapointAttribute::setAlias(const QString& alias) +{ + m_alias = alias; +} + +const QString& DatapointAttribute::type() const +{ + return m_type; +} + +void DatapointAttribute::setType(const QString& type) +{ + m_type = type; +} + +const QString& DatapointAttribute::unit() const +{ + return m_unit; +} + +void DatapointAttribute::setUnit(const QString& unit) +{ + m_unit = unit; +} + +const QString& DatapointAttribute::value() const +{ + return m_value; +} + +void DatapointAttribute::setValue(const QString& value) +{ + m_value = value; +} + +const double& DatapointAttribute::upperLimit() const +{ + return m_max; +} + +void DatapointAttribute::setUpperLimit(const double upperLimit) +{ + m_max = upperLimit; +} + +const double& DatapointAttribute::lowerLimit() const +{ + return m_min; +} + +void DatapointAttribute::setLowerLimit(const double lowerLimit) +{ + m_min = lowerLimit; +} + + +const QString DatapointAttribute::toString() const { + return "DatapointAttribute(" + QStringList({m_name, m_alias, m_type, m_unit, m_value}).join(", ") + ")"; +} + +bool DatapointAttribute::isBoundariesCheckEnabled(){ + return checkBoundaries; +} + +void DatapointAttribute::enableBoundariesCheck(){ + checkBoundaries = true; +} + +bool DatapointAttribute::isInRange(QString value){ + if(checkBoundaries){ + const double d_value = value.toDouble(); + return d_value > m_min && d_value < m_max; + }else{ + return false; + } +} + +DatapointAttribute readAttribute(const QDomElement &attributeEntry){ + DatapointAttribute attribute; + + attribute.m_alias = attributeEntry.attribute("alias", ""); + attribute.m_name = attributeEntry.attribute("name", ""); + attribute.m_type = attributeEntry.attribute("type", ""); + attribute.m_unit = attributeEntry.attribute("unit", ""); + attribute.m_value = attributeEntry.attribute("value", ""); + + + if(attributeEntry.hasAttribute("max")){ + attribute.enableBoundariesCheck(); + attribute.m_max = attributeEntry.attribute("max").toDouble(); + } + + if(attributeEntry.hasAttribute("min")){ + attribute.enableBoundariesCheck(); + attribute.m_min = attributeEntry.attribute("min").toDouble(); + } + return attribute; +} diff --git a/MAC/WinCCDBBridge/src/DatapointAttribute.hpp b/MAC/WinCCDBBridge/src/DatapointAttribute.hpp new file mode 100644 index 0000000000000000000000000000000000000000..ef1b223e0982d51799b82a549cc1725a52c597a3 --- /dev/null +++ b/MAC/WinCCDBBridge/src/DatapointAttribute.hpp @@ -0,0 +1,76 @@ +#ifndef DATAPOINT_ATTRIBUTE_H +#define DATAPOINT_ATTRIBUTE_H + +#include <QString> +class DatapointRule; +class QDomElement; + +/*! \class DatapointAttribute + \brief Define an attribute for a given datapoint. + The DatapointAttribute describes an attribute of a datapoint that could be for example a last modification date or a value. + If it is a value it can be subject of jittering. For this reason, it can be possible to specify a range within which the value + can be without triggering an update of the database. + It has to be specified as in the following XML fragment (ex. + <attribute name=":_original.._value" alias="value" type="STRING" unit=""/> + ) +*/ + + +class DatapointAttribute +{ +public: + DatapointAttribute(); + DatapointAttribute(QString name, QString alias, QString type, QString unit, QString value, double minValue, double maxValue, bool check); + + const QString formatValue() const; + const QString toString() const; + + const QString & URI() const; + void setURI(const QString &uri); + + bool isBoundariesCheckEnabled(); + void enableBoundariesCheck(); + + bool isInRange(QString value); + + const QString & name() const; + void setName(const QString & name); + + const QString & alias() const; + void setAlias(const QString & alias); + + const QString & type() const; + void setType(const QString & type); + + const QString & unit() const; + void setUnit(const QString & unit); + + const QString & value() const; + void setValue(const QString & value); + + const double & upperLimit() const; + void setUpperLimit(const double upperLimit); + + const double & lowerLimit() const; + void setLowerLimit(const double lowerLimit); + + void setParentRule(DatapointRule & rule); + +private: + QString m_name; + QString m_alias; + QString m_type; + QString m_unit; + QString m_value; + double m_max; + double m_min; + QString m_uri; + QString composeDatapointURI(DatapointRule & rule); + bool checkBoundaries; +friend DatapointAttribute readAttribute(const QDomElement &);; +}; + + +DatapointAttribute readAttribute(const QDomElement &attributeEntry); + +#endif // DATAPOINT_ATTRIBUTE_H diff --git a/MAC/WinCCDBBridge/src/DatapointIndex.cpp b/MAC/WinCCDBBridge/src/DatapointIndex.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fe952072768514ae72d3ae559611d2a357f9f158 --- /dev/null +++ b/MAC/WinCCDBBridge/src/DatapointIndex.cpp @@ -0,0 +1,52 @@ +#include "DatapointIndex.hpp" +#include "DatapointAttribute.hpp" + +#include <QRegularExpression> +#include <QDomElement> +#include <QDebug> + +DatapointIndex::DatapointIndex() +{ + +} + +const QString DatapointIndex::toString() const { + return "DatapointIndex(\"" + QStringList({pattern}).join(", ") + "\")"; +} + +void DatapointIndex::parseDPIndex(QString index) +{ + QRegularExpression regexParser{pattern}; + QRegularExpressionMatch matched = regexParser.match(index); + + + if(matched.hasMatch()){ + for(DatapointAttribute & key: m_keys){ + const QString index_id{key.name()}; + key.setValue(matched.captured(index_id)); + } + } + +} + +const std::vector<DatapointAttribute>& DatapointIndex::keys() +{ + return m_keys; +} + + + +const DatapointIndex readIndexRule(const QDomElement & element, const QString & dpName){ + DatapointIndex dpIndex; + + dpIndex.pattern = element.firstChildElement("regex").text(); + QDomNodeList attributes = element.elementsByTagName("attribute"); + for(int i=0; i < attributes.size(); i++){ + DatapointAttribute attribute{readAttribute(attributes.item(i).toElement())}; + dpIndex.m_keys.push_back(attribute); + } + + dpIndex.parseDPIndex(dpName); + + return dpIndex; +} diff --git a/MAC/WinCCDBBridge/src/DatapointIndex.hpp b/MAC/WinCCDBBridge/src/DatapointIndex.hpp new file mode 100644 index 0000000000000000000000000000000000000000..21cf9724234fdd006b23e73d12586669515bb36c --- /dev/null +++ b/MAC/WinCCDBBridge/src/DatapointIndex.hpp @@ -0,0 +1,40 @@ +#ifndef DATAPOINT_INDEX_H +#define DATAPOINT_INDEX_H +#include <QString> + +class DatapointAttribute; +class QDomElement; + +/*! \class DatapointIndex + \brief Define a unique index for a given datapoint. + The DatapointIndex is meant to reflect a DB table index. + Such index is derived from the datapoint name through a regex to give the widest range of possibilities in the configuration file. + In particular, the XML fragment parsed by the function readIndexRule and used to instantiate the DatapointIndex is for example: + <index> + <regex> + <![CDATA[(?<system>\w*)\.((?<attribute>\w*))]]> + </regex> + <attribute name="system" alias="system" type="STRING"/> + <attribute name="attribute" alias="attribute" type="STRING"/> + </index> + The index keys are picked up from the named regex capture group. Therefore, each capture group that has to be used as an index field + has to be named. +*/ + +class DatapointIndex +{ +public: + DatapointIndex(); + + void parseDPIndex(QString index); + const QString toString() const; + const std::vector<DatapointAttribute> & keys(); +private: + std::vector<DatapointAttribute> m_keys; + QString pattern; +friend const DatapointIndex readIndexRule(const QDomElement &, const QString &); +}; + +const DatapointIndex readIndexRule(const QDomElement & element, const QString & dpName); + +#endif // DATAPOINT_INDEX_H diff --git a/MAC/WinCCDBBridge/src/DatapointRule.cpp b/MAC/WinCCDBBridge/src/DatapointRule.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0c4663909d0217f97164c8f4c5b7fbc456341e24 --- /dev/null +++ b/MAC/WinCCDBBridge/src/DatapointRule.cpp @@ -0,0 +1,199 @@ +#include "DatapointRule.hpp" + +#include "DatapointIndex.hpp" +#include "DatapointAttribute.hpp" + +#include <QStringList> + +#include <QSqlQuery> +#include <QTimer> + +#include <QDebug> +#include <QDomElement> +#include <QDomDocument> + +DatapointRule::DatapointRule(): statement{""} +{ + +} + +std::vector<std::string> DatapointRule::getDatapointsURI() +{ + using std::vector; + using std::string; + + vector<string> datapointURIs; + for(auto & attr: m_attributes){ + datapointURIs.push_back(attr.second.URI().toStdString()); + } + return datapointURIs; +} + +DatapointRule::DatapointRule(QString name, QString alias, QString description): + m_name{name}, m_alias{alias}, m_description{description}, statement{""}, checkBoundaries{false} +{ + +} + +void DatapointRule::addAttribute(DatapointAttribute attribute){ + attribute.setParentRule(*this); + + checkBoundaries |= attribute.isBoundariesCheckEnabled(); + + m_attributes[attribute.URI().toStdString()] = attribute; +} + +const QString DatapointRule::toString() const { + return "DatapointRule(" + QStringList({m_name, m_alias, m_description}).join(", ") + ")"; +} + +const QString& DatapointRule::name() const +{ + return m_name; +} + +const QString& DatapointRule::alias() const +{ + return m_alias; +} + +const QString& DatapointRule::description() const +{ + return m_description; +} + +const QString& DatapointRule::table() const +{ + return m_table; +} + +void DatapointRule::setName(const QString& name) +{ + m_name = name; +} + +void DatapointRule::setAlias(const QString& alias) +{ + m_alias = alias; +} + +void DatapointRule::setDescription(const QString& description) +{ + m_description = description; +} +void DatapointRule::setTable(const QString& table) +{ + m_table = table; +} + + +void DatapointRule::prepareStatement(){ + QStringList columnNames; + QStringList valueNames; + QStringList indexNames; + + for(const DatapointAttribute & att : index.keys()){ + columnNames << att.alias(); + valueNames << att.formatValue(); + } + + for(const auto & att : m_attributes){ + columnNames << att.second.alias(); + valueNames << ":" + att.second.alias(); + + } + + statement = "INSERT INTO " + m_table + " "; + statement += "(" + columnNames.join(", ") + ")"; + statement += " VALUES "; + statement += "(" + valueNames.join(", ") + ")"; +} + +const QString & DatapointRule::sqlStatement(){ + if(statement.isEmpty() == true) prepareStatement(); + return statement; +} + +void DatapointRule::bindValues(QSqlQuery & query, std::map<std::string, std::string> & values){ + qDebug() << "binding values"; + + for(auto & uriValue: values){ + const QString attribute_alias = ":" + m_attributes[uriValue.first].alias(); + const QVariant attribute_value = QString::fromStdString(uriValue.second); + qDebug() << "alias=" << attribute_alias << ", value=" << attribute_value; + query.bindValue(attribute_alias, attribute_value); + } +} + +DatapointRule * readDatapointRule(const QDomElement & element){ + + DatapointRule * dpRule{new DatapointRule}; + dpRule->m_description = element.firstChildElement("description").text(); + + dpRule->m_checkpointInterval = std::chrono::seconds(element.firstChildElement("CheckpointInterval").attribute("value").toInt()); + dpRule->m_name = element.attribute("name"); + dpRule->m_alias = element.attribute("alias"); + + dpRule->index = readIndexRule(element.firstChildElement("index"), dpRule->m_name); + + qDebug() << dpRule->index.toString(); + + QDomNodeList attributes = element.firstChildElement("attributes").elementsByTagName("attribute"); + + for(int i=0; i < attributes.size(); i++){ + DatapointAttribute attribute{readAttribute(attributes.item(i).toElement())}; + qDebug()<<attribute.toString(); + dpRule->addAttribute(attribute); + } + return dpRule; +} + +bool DatapointRule::hasToTriggerUpdate(std::map<std::string, std::string> & values){ + if(checkBoundaries){ + qDebug() << "check values in range"; + bool inRange = true; + for(const auto uriValue: values){ + DatapointAttribute & attribute = m_attributes[uriValue.first]; + inRange = inRange && attribute.isInRange(uriValue.second.c_str()); + } + return !inRange; + }else{ + return true; + } +} + +void DatapointRule::triggerRequestCheckpoint(){ + emit requestCheckpoint(this); +} + +void DatapointRule::startCheckpointTimer(){ + triggerRequestCheckpoint(); + timer.reset(new QTimer(this)); + connect(timer.get(), &QTimer::timeout, this, &DatapointRule::triggerRequestCheckpoint); + if(m_checkpointInterval.count() > 0){ + timer->setInterval(std::chrono::milliseconds(m_checkpointInterval).count()); + timer->start(); + } +} + +std::vector<std::shared_ptr<DatapointRule>> readDatapointRules(QDomDocument & document){ + QDomElement table = document.firstChildElement("Table"); + QString tableName = table.attribute("name"); + + qDebug() << "Rule file for table " << tableName; + + QDomElement element = table.firstChildElement("Datapoints") + .firstChildElement("Datapoint"); + std::vector<std::shared_ptr<DatapointRule>> rules; + + while(element.isNull() == false){ + + DatapointRule * rule{readDatapointRule(element)}; + rule->m_table = tableName; + rule->prepareStatement(); + rules.push_back(std::shared_ptr<DatapointRule>{rule}); + + element = element.nextSiblingElement(); + } + return rules; +} diff --git a/MAC/WinCCDBBridge/src/DatapointRule.hpp b/MAC/WinCCDBBridge/src/DatapointRule.hpp new file mode 100644 index 0000000000000000000000000000000000000000..2e51506befe56e716739d8da24c4db9cb98156ff --- /dev/null +++ b/MAC/WinCCDBBridge/src/DatapointRule.hpp @@ -0,0 +1,77 @@ +#ifndef DATAPOINT_RULE_H +#define DATAPOINT_RULE_H + +#include <vector> +#include <map> +#include <memory> + +#include <QString> +#include <QObject> +#include <chrono> +class QTimer; +class QSqlQuery; +class QDomElement; +class QDomDocument; + +#include "DatapointAttribute.hpp" +#include "DatapointIndex.hpp" + +class DatapointRule: public QObject +{ + Q_OBJECT +public: + DatapointRule(); + DatapointRule(QString name, QString alias, QString description); + std::vector<std::string> getDatapointsURI(); + + std::chrono::seconds m_checkpointInterval; + + DatapointIndex index; + + const QString toString() const; + void addAttribute(DatapointAttribute attribute); + + const QString & sqlStatement(); + void bindValues(QSqlQuery & query, std::map<std::string, std::string> & values); + + bool hasToTriggerUpdate(std::map<std::string, std::string> & values); + + void prepareStatement(); + + const QString & name() const; + void setName(const QString & name); + + const QString & alias() const; + void setAlias(const QString & alias); + + const QString & description() const; + void setDescription(const QString & description); + + const QString & table() const; + void setTable(const QString & table); +private: + QString m_name; + QString m_alias; + QString m_description; + QString m_table; + + QString statement; + std::map<std::string, DatapointAttribute> m_attributes; + std::shared_ptr<QTimer> timer; + bool checkBoundaries; +signals: + void requestCheckpoint(DatapointRule *); + +public slots: + void triggerRequestCheckpoint(); + void startCheckpointTimer(); + +friend DatapointRule * readDatapointRule(const QDomElement &); +friend std::vector<std::shared_ptr<DatapointRule>> readDatapointRules(QDomDocument &); +}; + +DatapointRule * readDatapointRule(const QDomElement & element); +std::vector<std::shared_ptr<DatapointRule>> readDatapointRules(QDomDocument & document); + +#endif // DATAPOINT_RULE_H + diff --git a/MAC/WinCCDBBridge/src/Program.cpp b/MAC/WinCCDBBridge/src/Program.cpp new file mode 100644 index 0000000000000000000000000000000000000000..85ddba1a1dea6793017afc40e3a10cd95b72b5fc --- /dev/null +++ b/MAC/WinCCDBBridge/src/Program.cpp @@ -0,0 +1,251 @@ +#include "Program.hpp" + +#include <QDebug> +#include <QFile> +#include <QJsonDocument> +#include <QJsonArray> +#include <QJsonObject> +#include <QJsonValue> + +#include <QDomDocument> +#include <QDomElement> +#include <QDomNode> + +#include <QXmlInputSource> + +#include <QtNetwork> +#include <QUrl> +#include <QSqlQuery> +#include <QSqlError> + + +#include <exception> + +using LOFAR::WINCCWRAPPER::WinCCWrapper; + +class ConfigurationFileReadError : public std::exception { +private: + std::string message; +public: + ConfigurationFileReadError(std::string path): message{"Error reading file " + path} {} + + const char * what() const throw () { + return message.c_str(); + } +}; + +Program::Program(QObject * parent, const QString& configurationFile, const QString & projectName): QObject{parent}, + database{new QSqlDatabase} +{ + wrapper.reset(new LOFAR::WINCCWRAPPER::WinCCWrapper{"Program", projectName.toStdString(), 0}); + + try{ + readConfigurationFile(configurationFile); + + // Event connect + // -- WINCC side + wrapper->set_connect_datapoints_callback([=](std::map<std::string, std::string> values){ + emit datapointChanged(values); }); + // -- QT side + connect(this, &Program::datapointChanged, this, &Program::consolePrint); + connect(this, &Program::datapointChanged, this, &Program::messageDispatch); + } + catch (std::exception & e){ + qCritical() << "An error occurred while reading the configuration file: "<< e.what(); + qFatal("Cannot read configuration file"); + } + +} + +Program::~Program() +{ +} + +void Program::run() +{ + database->open(); + connectToDatapoints(); + emit startRulesCheckpointTriggers(); + + while(true){ + qApp->processEvents(); + wrapper->wait_for_event(0, 500); + } + wrapper->exit(); + database->close(); + qInfo()<< "Closing worker threads"; + +} + +void Program::consolePrint(std::map<std::string, std::string> values) +{ + + qDebug() << "Received: "; + for(const auto & value: values){ + qDebug()<< value.first.c_str() << " " << value.second.c_str(); + } +} + +void Program::messageDispatch(std::map<std::string, std::string> values) +{ + if(values.size() == 0) return; + + const std::string & uri = values.begin()->first; + + + DatapointRule * rule = uriToRule[uri]; + if(rule == nullptr) qWarning() << "CANNOT FIND RULE FOR"<< uri.c_str(); + + qInfo() << "update has been triggered for" <<uri.c_str() << "with name " << rule->name(); + if(rule->hasToTriggerUpdate(values)){ + + const QString queryString{rule->sqlStatement()}; + QSqlDatabase db{QSqlDatabase::database()}; + + QSqlQuery query{db}; + query.prepare(queryString); + + QMapIterator<QString, QVariant> boundValuesIt(query.boundValues()); + + while(boundValuesIt.hasNext()){ + boundValuesIt.next(); + qDebug()<< "key" << boundValuesIt.key()<< "value" << boundValuesIt.value(); + } + + rule->bindValues(query, values); + + + if(query.exec() != true) qWarning() << query.lastError(); + + qInfo() << "Executed query " << query.executedQuery(); + qInfo() << rule->name() << " SQL:" << queryString; + }else{ + qInfo() << "Skipping update of "<< uri.c_str() << " with rule name " << rule->name() << "values all in range"; + } + +} + +const QJsonDocument readXMLFile(QFile & file){ + + QString buffer; + + file.open(QIODevice::ReadOnly | QIODevice::Text); + try{ + buffer = file.readAll(); + file.close(); + }catch(std::exception &e){ + file.close(); + qCritical()<<"An error occurred while reading configuration file"<< e.what(); + throw e; + } + return QJsonDocument::fromJson(buffer.toUtf8()); +} + +void Program::readDBConfigurationSection(QSettings & configuration){ + + configuration.beginGroup("Database"); + const QString name = configuration.value("name").toString(); + const QString type = configuration.value("type").toString(); + const QString address = configuration.value("address").toString(); + const int port = configuration.value("port").toInt(); + const QString user = configuration.value("user").toString(); + const QString pass = configuration.value("pass").toString(); + configuration.endGroup(); + + QSqlDatabase db = QSqlDatabase::addDatabase(type); + + db.setHostName(address); + db.setDatabaseName(name); + db.setPort(port); + db.setUserName(user); + db.setPassword(pass); + + qInfo()<<db; + + *database = db; +} + +void Program::readConfigurationFile(QString fileName){ + QFile pathToConfigFile{fileName}; + + if(pathToConfigFile.exists()){ + QSettings configuration{fileName, QSettings::IniFormat}; + readDBConfigurationSection(configuration); + + }else{ + throw ConfigurationFileReadError{fileName.toStdString()}; + } +} + +void Program::connectToDatapoints(){ + + std::cout<< "Datapoint rules are " << dataPointRules.size(); + + for(std::shared_ptr<DatapointRule> & rule: dataPointRules){ + std::vector<std::string> dataPoints{rule->getDatapointsURI()}; + + for(std::string name : dataPoints) qDebug()<< "Registering " << dataPoints.size() << "rule "<< name.c_str(); + wrapper->connect_datapoints_multi(dataPoints); + } +} + +void Program::checkpointRequested(DatapointRule * rule){ + + std::map<std::string, std::string> values; + qInfo() << "Processing rule " << rule->name(); + try { + + for(const std::string & uri: rule->getDatapointsURI()){ + values[uri] = wrapper->get_formatted_datapoint(uri); + qDebug() << "uri:"<<uri.c_str() << "\t" << wrapper->get_formatted_datapoint(uri).c_str(); + } + + QSqlDatabase db = QSqlDatabase::database(); + QSqlQuery query{db}; + query.prepare(rule->sqlStatement()); + + rule->bindValues(query, values); + if(!query.exec()) qWarning() << query.lastError(); + qInfo() << "Processed rule " << rule->name(); + + qDebug() << "Executed query " << query.executedQuery(); + }catch (...) { + qWarning() << "cannot update rule " << rule->name(); + } +} + + + +QDomDocument readFile(QString fileName){ + QFile datapointFile{fileName}; + + if(datapointFile.exists()){ + + QDomDocument parsedFile; + QXmlInputSource sourceXML{&datapointFile}; + parsedFile.setContent(&sourceXML, true); + datapointFile.close(); + return parsedFile; + + }else{ + throw ConfigurationFileReadError{fileName.toStdString()}; + } +} + +void Program::loadDatapointsConfiguration(QString configurationFile) +{ + QDomDocument parsedContent{readFile(configurationFile)}; + + dataPointRules = readDatapointRules(parsedContent); + for(std::shared_ptr<DatapointRule> & rule: dataPointRules){ + connect(rule.get(), &DatapointRule::requestCheckpoint, this, &Program::checkpointRequested); + connect(this, &Program::startRulesCheckpointTriggers, rule.get(), &DatapointRule::startCheckpointTimer); + + for(std::string uri: rule->getDatapointsURI()){ + uriToRule[uri] = rule.get(); + qDebug()<< "URI="<< uri.c_str() << "SQL: " << rule->sqlStatement(); + } + } +} + +#include "Program.moc" diff --git a/MAC/WinCCDBBridge/src/Program.hpp b/MAC/WinCCDBBridge/src/Program.hpp new file mode 100644 index 0000000000000000000000000000000000000000..232e6c18a5b855c10360096ac7cf243a4bba6a21 --- /dev/null +++ b/MAC/WinCCDBBridge/src/Program.hpp @@ -0,0 +1,50 @@ +#ifndef PROGRAM_H +#define PROGRAM_H + +#include <QObject> +#include <QSqlDatabase> + +#include <WinCCWrapper.h> +#include <memory> + +#include <map> +#include <QString> + +#include "DatapointRule.hpp" + +class QSettings; + +class Program : public QObject +{ + Q_OBJECT + +public: + Program(QObject * parent, const QString& configurationFile, const QString & projectName); + void loadDatapointsConfiguration(QString configurationFile); + virtual ~Program(); + void run(); + +public slots: + void consolePrint(std::map<std::string, std::string> datapoints); //!< Prints a log message for each changed datapoint name + void messageDispatch(std::map<std::string, std::string> datapoints); //!< Send the datapoint change value into the database + void checkpointRequested(DatapointRule *); //! < React when a rule triggers a checkpoint request + +signals: + void datapointChanged(std::map<std::string, std::string> datapoints); //!< This event is triggered when a registered datapoint value changes + void startRulesCheckpointTriggers(); + +private: + std::shared_ptr<LOFAR::WINCCWRAPPER::WinCCWrapper> wrapper; //!< Pointer to an instance of the WinCCWrapper + + void readConfigurationFile(QString fileName); //!< Read the configuration file + void readDBConfigurationSection(QSettings &); //!< Read the database section of the configuration file + + void startWorkers(int num); + void connectToDatapoints(); //! Ask WinCC to get an update every time one of the specified datapoint changes + std::map<std::string, DatapointRule *> uriToRule; + std::vector<std::shared_ptr<DatapointRule>> dataPointRules; + std::shared_ptr<QSqlDatabase> database; + +}; + +#endif // PROGRAM_H diff --git a/MAC/WinCCDBBridge/src/main.cpp b/MAC/WinCCDBBridge/src/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b7ed825afac36f5f5fdd085655cec22e3fb64acb --- /dev/null +++ b/MAC/WinCCDBBridge/src/main.cpp @@ -0,0 +1,69 @@ +#include <QCoreApplication> + +#include <QCommandLineParser> +#include <QDebug> + +#include "Program.hpp" + +/** +Defines the Qt application properties. +*/ +void defineApplication(){ + QCoreApplication::setApplicationName("WinCCDispatcher"); + +} + +/** +Defines the command line options +*/ +void defineCommandLineOptions(QCommandLineParser & parser){ + parser.setApplicationDescription("Stores the value directly in a chosen SQL database.\n"); + + parser.addHelpOption(); + parser.addPositionalArgument("datapoint_config", "Configuration file containing the list of datapoints to connect to"); + parser.addPositionalArgument("configuration", "Configuration file containing the database connection info"); + parser.addPositionalArgument("WinCCProjectName", "Project name to connect to"); +} + + +/** +Execute the main program +*/ +void executeProgram(const QStringList arguments){ + const QString datapointXMLConfiguration = arguments.at(0); + const QString configuration = arguments.at(1); + + const QString winccProjectName = arguments.at(2); + + Program program{nullptr, configuration, winccProjectName}; + program.loadDatapointsConfiguration(datapointXMLConfiguration); + program.run(); +} + +/** +Main function. It execute in succession: + * parsing the program options + * handle required missing parameter + * constructing the main program + * executes the main loop +*/ +int main(int argc, char * argv[]){ + QCoreApplication app{argc, argv}; + QCommandLineParser parser; + + defineApplication(); + + defineCommandLineOptions(parser); + + parser.process(app); + const QStringList arguments = parser.positionalArguments(); + + if(arguments.size() == 3) { + executeProgram(arguments); + }else{ + qCritical()<< "Missing required arguments"; + parser.showHelp(); + exit(1); + } + +} diff --git a/MAC/WinCCDBBridge/test/CMakeLists.txt b/MAC/WinCCDBBridge/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ddbb3222fc3143b96dbeeca40f7e0eb5e4d28f51 --- /dev/null +++ b/MAC/WinCCDBBridge/test/CMakeLists.txt @@ -0,0 +1,27 @@ +include(LofarCTest) + +IF(HAVE_UNITTEST++) + find_package(Qt5 COMPONENTS Core Network Sql Xml REQUIRED) + + include_directories(${Qt5_INCLUDE_DIRS}) + set(TESTED_FILES + ../src/DatapointAttribute.cpp + ../src/DatapointRule.cpp + ../src/DatapointIndex.cpp) + + set(TEST_NAMES + t_DatapointAttribute + t_DatapointIndex + ) + + foreach(TEST_NAME ${TEST_NAMES}) + lofar_add_test("${TEST_NAME}" "${TEST_NAME}.cpp" ${TESTED_FILES}) + target_link_libraries("${TEST_NAME}" + Qt5::Core + Qt5::Network + Qt5::Sql + Qt5::Xml + ) + endforeach() + +ENDIF(HAVE_UNITTEST++) diff --git a/MAC/WinCCDBBridge/test/t_DatapointAttribute.cpp b/MAC/WinCCDBBridge/test/t_DatapointAttribute.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5bd40df794b6f8af44aec0d7681c324cec695e6b --- /dev/null +++ b/MAC/WinCCDBBridge/test/t_DatapointAttribute.cpp @@ -0,0 +1,98 @@ +#include "UnitTest++/UnitTest++.h" + +#include "../src/DatapointAttribute.hpp" +#include "../src/DatapointRule.hpp" + +#include <QDomElement> +#include <QDomDocument> + + +#include <QDebug> +TEST(test_formatValue) +{ + DatapointAttribute attr("datapointname", "alias_name", "STRING", "s", "12", 0, 15, true); + CHECK_EQUAL("\'12\'", attr.formatValue().toStdString()); + + attr.setType("INT"); + + CHECK_EQUAL("12", attr.formatValue().toStdString()); +} + +TEST(test_toString) +{ + DatapointAttribute attr("datapointname", "alias_name", "STRING", "s", "12", 0, 15, true); + + CHECK_EQUAL("DatapointAttribute(datapointname, alias_name, STRING, s, 12)", attr.toString().toStdString()); +} + +TEST(test_isInRange) +{ + DatapointAttribute attr("datapointname", "alias_name", "STRING", "s", "12", 0, 15, true); + CHECK_EQUAL(true, attr.isInRange("12")); + + CHECK_EQUAL(false, attr.isInRange("-1")); + + CHECK_EQUAL(false, attr.isInRange("18")); + + attr = DatapointAttribute{}; + attr.setUpperLimit(12); + attr.setLowerLimit(0); + + CHECK_EQUAL(false, attr.isBoundariesCheckEnabled()); + CHECK_EQUAL(false, attr.isInRange("-1")); + CHECK_EQUAL(false, attr.isInRange("15")); + CHECK_EQUAL(false, attr.isInRange("5")); + +} + +TEST(test_URI){ + DatapointAttribute attr("datapointname", "alias_name", "STRING", "s", "12", 0, 15, true); + + DatapointRule rule("rule.", "ruleAlias", "mytestrule"); + + attr.setParentRule(rule); + + CHECK_EQUAL("rule.datapointname", attr.URI().toStdString()); +} + +TEST(test_XMLParsing){ + QDomDocument doc = QDomDocument("someParentElement"); + QDomElement test = doc.createElement("attribute"); + test.setAttribute("name", "myname"); + test.setAttribute("alias", "alias"); + test.setAttribute("unit", "s"); + test.setAttribute("value", "5"); + + test.setAttribute("max", "12"); + test.setAttribute("min", "12"); + + DatapointAttribute attr = readAttribute(test); + + CHECK_EQUAL("myname", attr.name().toStdString()); + CHECK_EQUAL("5", attr.value().toStdString()); + CHECK_EQUAL("alias", attr.alias().toStdString()); + CHECK_EQUAL("s", attr.unit().toStdString()); + CHECK_EQUAL(12, attr.upperLimit()); + CHECK_EQUAL(12, attr.lowerLimit()); + CHECK_EQUAL(true, attr.isBoundariesCheckEnabled()); + + + test = doc.createElement("attribute"); + test.setAttribute("name", "myname"); + test.setAttribute("alias", "alias"); + test.setAttribute("unit", "s"); + + attr = readAttribute(test); + + CHECK_EQUAL("myname", attr.name().toStdString()); + CHECK_EQUAL("", attr.value().toStdString()); + CHECK_EQUAL("alias", attr.alias().toStdString()); + CHECK_EQUAL("s", attr.unit().toStdString()); + CHECK_EQUAL(false, attr.isBoundariesCheckEnabled()); + +} + +int main(int, const char *[]) +{ + return UnitTest::RunAllTests(); +} diff --git a/MAC/WinCCDBBridge/test/t_DatapointIndex.cpp b/MAC/WinCCDBBridge/test/t_DatapointIndex.cpp new file mode 100644 index 0000000000000000000000000000000000000000..47acc3b9208400570578409bce27ab1eb0d89ab4 --- /dev/null +++ b/MAC/WinCCDBBridge/test/t_DatapointIndex.cpp @@ -0,0 +1,11 @@ +#include "UnitTest++/UnitTest++.h" + +#include "../src/DatapointAttribute.hpp" +#include "../src/DatapointIndex.hpp" +#include "../src/DatapointRule.hpp" + + +int main(int, const char *[]) +{ + return UnitTest::RunAllTests(); +}