diff --git a/contrib/html/openapi.yaml b/contrib/html/openapi.yaml index f8fbd219c..26ec93a57 100644 --- a/contrib/html/openapi.yaml +++ b/contrib/html/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: ebusd-http description: The API that ebusd provides on HTTP port. - version: "23.2" + version: "23.3" servers: - url: http://127.0.0.1:8080/ paths: @@ -145,6 +145,50 @@ paths: 500: description: General error. content: { } + /templates: + get: + summary: Get all known field templates for the root. + responses: + 200: + description: Success. + content: + application/json;charset=utf-8: + schema: + $ref: '#/components/schemas/Templates' + 400: + description: Invalid request parameters. + content: { } + 403: + description: User not authorized. + content: { } + 500: + description: General error. + content: { } + /templates/{path}: + get: + summary: Get all known field templates for the path. + parameters: + - name: path + in: path + required: true + schema: + type: string + responses: + 200: + description: Success. + content: + application/json;charset=utf-8: + schema: + $ref: '#/components/schemas/Templates' + 400: + description: Invalid request parameters. + content: { } + 403: + description: User not authorized. + content: { } + 500: + description: General error. + content: { } /raw: get: summary: Retrieve raw data from grabbed and/or decoded messages. @@ -392,8 +436,12 @@ components: type: integer minimum: 0 condition: - type: string - description: the condition string in case of a conditional message (only with full). + description: the condition(s) in case of a conditional message (only with full). + oneOf: + - $ref: '#/components/schemas/Condition' + - type: array + items: + $ref: '#/components/schemas/Condition' lastup: $ref: '#/components/schemas/Seconds' description: the time in UTC seconds of the last update of the message (0 @@ -407,8 +455,12 @@ components: description: destination master or slave address. example: 8 id: - $ref: '#/components/schemas/Symbols' - description: the message ID composed of PBSB and further master data bytes (only with def). + description: the message ID composed of PBSB and further master data bytes (only with def), or an array thereof in case of a chained message. + oneOf: + - $ref: '#/components/schemas/Symbols' + - type: array + items: + $ref: '#/components/schemas/Symbols' comment: type: string description: the message comment (only with verbose). @@ -465,10 +517,10 @@ components: - type: string - type: number nullable: true - FieldDef: + FieldTemplate: + description: a single field template. required: - name - - slave - type - isbits - length @@ -477,9 +529,6 @@ components: name: type: string description: the field name. - slave: - type: boolean - description: whether the field is part of the slave data. type: type: string description: the field type. @@ -513,6 +562,66 @@ components: comment: type: string description: the field comment. + FieldDef: + description: a single field definition. + allOf: + - $ref: '#/components/schemas/FieldTemplate' + - type: object + required: + - slave + properties: + slave: + type: boolean + description: whether the field is part of the slave data. + Templates: + type: array + description: list of known field templates. + items: + oneOf: + - $ref: '#/components/schemas/FieldTemplate' + - type: object + required: + - name + - sequence + properties: + name: + type: string + description: the template set name. + sequence: + type: array + description: the sequence of fields. + items: + $ref: '#/components/schemas/FieldTemplate' + Condition: + description: a single condition. + required: + - name + - message + properties: + name: + type: string + description: name of the condition. + message: + type: string + description: name of the referenced message. + circuit: + type: string + description: name of the referenced circuit. + zz: + maximum: 255 + minimum: 0 + type: integer + description: the circuit slave address. + field: + type: string + description: the field name in the referenced message. + value: + type: array + description: the value ranges for the condition. + items: + oneOf: + - type: number + - type: string DataType: description: a known field data type. type: object diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp index f78270f65..5995c8ee3 100644 --- a/src/ebusd/main.cpp +++ b/src/ebusd/main.cpp @@ -352,7 +352,14 @@ int main(int argc, char* argv[], char* envp[]) { } else { logNotice(lf_main, "configuration dump:"); } + *out << "{\"datatypes\":["; + DataTypeList::getInstance()->dump(s_opt.dumpConfig, true, out); + *out << "],\"templates\":["; + const auto tmpl = s_scanHelper->getTemplates(""); + tmpl->dump(s_opt.dumpConfig, out); + *out << "],\"messages\":"; s_messageMap->dump(true, s_opt.dumpConfig, out); + *out << "}"; if (fout.is_open()) { fout.close(); } diff --git a/src/ebusd/mainloop.cpp b/src/ebusd/mainloop.cpp index 3d35a3765..5da4dbab8 100644 --- a/src/ebusd/mainloop.cpp +++ b/src/ebusd/mainloop.cpp @@ -2254,6 +2254,18 @@ result_t MainLoop::executeGet(const vector& args, bool* connected, ostri return formatHttpResult(ret, type, ostream); } + if (uri == "/templates" || uri.substr(0, 11) == "/templates/") { + *ostream << "["; + OutputFormat verbosity = OF_NAMES|OF_JSON|OF_ALL_ATTRS; + string name = uri == "/templates" ? "" : uri.substr(11) + "/"; + const auto tmpl = m_scanHelper->getTemplates(name); + tmpl->dump(verbosity, ostream); + *ostream << "\n]"; + type = 6; + *connected = false; + return formatHttpResult(ret, type, ostream); + } + if (uri == "/raw") { time_t since = 0, until = 0; bool onlyUnknown = false; diff --git a/src/ebusd/scan.cpp b/src/ebusd/scan.cpp index 9e337fe05..4309cc42c 100644 --- a/src/ebusd/scan.cpp +++ b/src/ebusd/scan.cpp @@ -202,6 +202,19 @@ bool ScanHelper::readTemplates(const string relPath, const string extension, boo return false; } +void ScanHelper::dumpTemplates(OutputFormat outputFormat, ostream* output) const { + bool prependSeparator = false; + for (auto it : m_templatesByPath) { + if (prependSeparator) { + *output << ","; + } + const auto templates = it.second; + if (templates->dump(outputFormat, output)) { + prependSeparator = true; + } + } +} + result_t ScanHelper::readConfigFiles(const string& relPath, const string& extension, bool recursive, string* errorDescription) { vector files, dirs; diff --git a/src/ebusd/scan.h b/src/ebusd/scan.h index bc8cecf58..6cb4d6c3c 100644 --- a/src/ebusd/scan.h +++ b/src/ebusd/scan.h @@ -158,6 +158,13 @@ class ScanHelper : public Resolver { */ bool readTemplates(const string relPath, const string extension, bool available); + /** + * Dump the loaded @a DataFieldTemplates to the output. + * @param outputFormat the @a OutputFormat options. + * @param output the @a ostream to dump to. + */ + void dumpTemplates(OutputFormat outputFormat, ostream* output) const; + /** * Read the configuration files from the specified path. * @param relPath the relative path from which to read the files (without trailing "/"). diff --git a/src/lib/ebus/data.cpp b/src/lib/ebus/data.cpp index 0e9d38bcd..f463ee70e 100644 --- a/src/lib/ebus/data.cpp +++ b/src/lib/ebus/data.cpp @@ -482,7 +482,10 @@ void SingleDataField::dumpPrefix(bool prependFieldSeparator, OutputFormat output dumpString(prependFieldSeparator, m_name, output); } if (outputFormat & OF_JSON) { - *output << ", \"slave\": " << (m_partType == pt_slaveData ? "true" : "false") << ", "; + if (m_partType != pt_any) { + *output << ", \"slave\": " << (m_partType == pt_slaveData ? "true" : "false"); + } + *output << ", "; } else { *output << FIELD_SEPARATOR; if (m_partType == pt_masterData) { @@ -1459,4 +1462,27 @@ const DataField* DataFieldTemplates::get(const string& name) const { return ref->second; } +bool DataFieldTemplates::dump(OutputFormat outputFormat, ostream* output) const { + bool prependFieldSeparator = false; + for (const auto &it : m_fieldsByName) { + const DataField *dataField = it.second; + if (outputFormat & OF_JSON) { + if (dataField->isSet()) { + if (prependFieldSeparator) { + *output << ",\n"; + } + *output << "{\"name\":\"" << dataField->getName(-1) << "\", \"sequence\": ["; + dataField->dump(false, outputFormat, output); + *output << "]}"; + } else { + dataField->dump(prependFieldSeparator, outputFormat, output); + } + } else { + dataField->dump(prependFieldSeparator, outputFormat, output); + } + prependFieldSeparator = true; + } + return !prependFieldSeparator; +} + } // namespace ebusd diff --git a/src/lib/ebus/data.h b/src/lib/ebus/data.h index f205509a8..810c34884 100755 --- a/src/lib/ebus/data.h +++ b/src/lib/ebus/data.h @@ -836,6 +836,14 @@ class DataFieldTemplates : public MappedFileReader { */ const DataField* get(const string& name) const; + /** + * Dump the templates to the output. + * @param outputFormat the @a OutputFormat options. + * @param output the @a ostream to dump to. + * @return true when a template was written to the output. + */ + bool dump(OutputFormat outputFormat, ostream* output) const; + private: /** the known template @a DataField instances by name. */ diff --git a/src/lib/ebus/message.cpp b/src/lib/ebus/message.cpp index 54a634fa5..1c77a7fd0 100644 --- a/src/lib/ebus/message.cpp +++ b/src/lib/ebus/message.cpp @@ -970,9 +970,8 @@ void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, b *output << ",\n \"pollprio\": " << setw(0) << dec << getPollPriority(); } if (isConditional()) { - *output << ",\n \"condition\": \""; - m_condition->dump(false, output); - *output << "\""; + *output << ",\n \"condition\": "; + m_condition->dumpJson(output); } } if (withData) { @@ -985,13 +984,8 @@ void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, b } *output << ",\n \"zz\": " << dec << static_cast(m_dstAddress); if (withDefinition) { - *output << ",\n \"id\": [" << dec; - for (auto it = m_id.begin(); it < m_id.end(); it++) { - if (it > m_id.begin()) { - *output << ", "; - } - *output << dec << static_cast(*it); - } + *output << ",\n \"id\": ["; + dumpIdsJson(output); *output << "]"; } appendAttributes(outputFormat, output); @@ -1022,6 +1016,15 @@ void Message::decodeJson(bool leadingSeparator, bool appendDirectionCondition, b *output << "\n }"; } +void Message::dumpIdsJson(ostringstream* output) const { + for (auto it = m_id.begin(); it < m_id.end(); it++) { + if (it > m_id.begin()) { + *output << ", "; + } + *output << dec << static_cast(*it); + } +} + bool Message::setDataHandlerState(int state, bool addBits) { if (addBits ? state == (m_dataHandlerState&state) : state == m_dataHandlerState) { return false; @@ -1296,6 +1299,22 @@ void ChainedMessage::dumpField(const string& fieldName, bool withConditions, Out } } +void ChainedMessage::dumpIdsJson(ostringstream* output) const { + for (auto idsit = m_ids.begin(); idsit < m_ids.end(); idsit++) { + if (idsit > m_ids.begin()) { + *output << ","; + } + *output << "["; + for (auto it = (*idsit).begin(); it < (*idsit).end(); it++) { + if (it > (*idsit).begin()) { + *output << ", "; + } + *output << dec << static_cast(*it); + } + *output << "]"; + } +} + /** * Get the first available @a Message from the list. @@ -1527,6 +1546,27 @@ void SimpleCondition::dump(bool matched, ostream* output) const { } } +void SimpleCondition::dumpJson(ostream* output) const { + *output << "{\"name\": \"" << m_refName << "\""; + if (!m_circuit.empty()) { + *output << ",\"circuit\":\"" << m_circuit << "\""; + } + *output << ",\"message\":\"" << (m_name.empty() ? "scan" : m_name) << "\""; + if (m_dstAddress != SYN) { + *output << ",\"zz\":" << dec << static_cast(m_dstAddress); + } + if (!m_field.empty()) { + *output << ",\"field\":\"" << m_field << "\""; + } + if (m_hasValues) { + *output << ",\"value\":["; + dumpValuesJson(output); + *output << "]"; + } + *output << "}"; +} + + CombinedCondition* SimpleCondition::combineAnd(Condition* other) { CombinedCondition* ret = new CombinedCondition(); return ret->combineAnd(this)->combineAnd(other); @@ -1625,6 +1665,17 @@ bool SimpleNumericCondition::checkValue(const Message* message, const string& fi return false; } +void SimpleNumericCondition::dumpValuesJson(ostream* output) const { + bool first = true; + for (const auto value : m_valueRanges) { + if (!first) { + *output << ","; + } + *output << static_cast(value); + first = false; + } +} + bool SimpleStringCondition::checkValue(const Message* message, const string& field) { ostringstream output; @@ -1641,6 +1692,17 @@ bool SimpleStringCondition::checkValue(const Message* message, const string& fie return false; } +void SimpleStringCondition::dumpValuesJson(ostream* output) const { + bool first = true; + for (const auto value : m_values) { + if (!first) { + *output << ","; + } + *output << "\"" << value << "\""; + first = false; + } +} + void CombinedCondition::dump(bool matched, ostream* output) const { for (const auto condition : m_conditions) { @@ -1648,6 +1710,19 @@ void CombinedCondition::dump(bool matched, ostream* output) const { } } +void CombinedCondition::dumpJson(ostream* output) const { + *output << "["; + bool first = true; + for (const auto condition : m_conditions) { + if (!first) { + *output << ","; + } + condition->dumpJson(output); + first = false; + } + *output << "]"; +} + result_t CombinedCondition::resolve(void (*readMessageFunc)(Message* message), MessageMap* messages, ostringstream* errorMessage) { for (const auto condition : m_conditions) { @@ -2817,7 +2892,7 @@ void MessageMap::dump(bool withConditions, OutputFormat outputFormat, ostream* o bool first = true; bool isJson = (outputFormat & OF_JSON) != 0; if (isJson) { - *output << (m_addAll ? "[" : "}"); + *output << (m_addAll ? "[" : "{"); } else { Message::dumpHeader(nullptr, output); } diff --git a/src/lib/ebus/message.h b/src/lib/ebus/message.h index fcd60ac9b..7d314670f 100644 --- a/src/lib/ebus/message.h +++ b/src/lib/ebus/message.h @@ -601,6 +601,12 @@ class Message : public AttributedItem { OutputFormat outputFormat, ostringstream* output) const; protected: + /** + * Dump the ID(s) to the output in JSON format. + * @param output the @a ostringstream to append to. + */ + virtual void dumpIdsJson(ostringstream* output) const; + /** the source filename. */ const string m_filename; @@ -748,6 +754,8 @@ class ChainedMessage : public Message { result_t prepareMasterPart(size_t index, const char separator, istringstream* input, MasterSymbolString* master) override; + // @copydoc + void dumpIdsJson(ostringstream* output) const override; public: // @copydoc @@ -884,6 +892,18 @@ class Condition { */ virtual void dump(bool matched, ostream* output) const = 0; + /** + * Write the condition definition in JSON to the @a ostream. + * @param output the @a ostream to append to. + */ + virtual void dumpJson(ostream* output) const = 0; + + /** + * Write the values part of the condition definition in JSON to the @a ostream. + * @param output the @a ostream to append to. + */ + virtual void dumpValuesJson(ostream* output) const { /* empty on top level*/ }; + /** * Combine this condition with another instance using a logical and. * @param other the @a Condition to combine with. @@ -951,6 +971,9 @@ class SimpleCondition : public Condition { // @copydoc void dump(bool matched, ostream* output) const override; + // @copydoc + void dumpJson(ostream* output) const override; + // @copydoc CombinedCondition* combineAnd(Condition* other) override; @@ -1043,6 +1066,9 @@ class SimpleNumericCondition : public SimpleCondition { // @copydoc bool checkValue(const Message* message, const string& field) override; + // @copydoc + void dumpValuesJson(ostream* output) const override; + private: /** the valid value ranges (pairs of from/to inclusive), empty for @a m_message seen check. */ @@ -1084,6 +1110,9 @@ class SimpleStringCondition : public SimpleCondition { // @copydoc bool checkValue(const Message* message, const string& field) override; + // @copydoc + void dumpValuesJson(ostream* output) const override; + private: /** the valid values. */ @@ -1110,6 +1139,9 @@ class CombinedCondition : public Condition { // @copydoc void dump(bool matched, ostream* output) const override; + // @copydoc + void dumpJson(ostream* output) const override; + // @copydoc CombinedCondition* combineAnd(Condition* other) override { m_conditions.push_back(other); return this; }