OData API
Introduction
OData (Open Data Protocol) is a standard that defines a way of creating and using RESTful APIs. Vtrin Server implements the basic functionality of OData (OData Minimal Conformance Level) plus some extra features.
The API uses any authentication that the Vtrin Server has been configured to use. All examples on this page assume usage of the necessary Authorization header with requests and that all the necessary certificates have been set up.
OData features
OData API is good for doing calculations and analytics on top of History. First a quick recap of OData features as shown on External Interfaces feature comparison:
With OData we can:
- Read classes and instances (however, only classes with name or Id are visible)
- Create and update classes and instances
- Read and write time series data
- Read and write current values
However, we cannot use OData to subscribe current values.
Further reading:The fourth version of the protocol is specified here.
For new OData user please visit this tutorial page first.
Documentation summary
This documentation includes:
- How to get started with OData.
- How to change the shape of the returned data.
- How you can read (GET functions) and write (POST actions) time-series data using OData actions and functions.
- We provide some example queries and example programs written in Python, R, and C#.
- Finally, we give some examples of Power Bi reports with OData.
OData tutorials & problem solving
If you want to refer how-to guides, we have prepared some tutorials with examples of OData API use.
- OData tutorial: CRUD will give examples of CRUD operations done with Chrome's Console.
- OData API tutorial: Power BI report will give step by step guide on how to create Power BI report using ABB Ability™ History OData API.
- OData Problem Solving will guide you through possible issues with Power Queries, Power BI OData Feed and Change Log.
- Getting started with OData requests in Excel.
Getting started
APIs can be scary sometimes, but no worries. This section is here to give some basic understanding of how to use Vtrin Server OData REST API. In this section we do no magic, just normal GET calls to the host server.
The OData interface consists of services that are described at the service root. The service root defaults to http[s]://[hostname]/odata/, and all other services reside inside the service root. A more thorough description of the services is available under http[s]://[hostname]/odata/$metadata, which describes the structure of the services.
Note to userWe kindly remind that Vtrin OData interface is not enabled by default. If it is not enabled, you may enable it by adding or setting
EnableODataparameter totruein the appSettings section available in the Vtrin Server Configuration file.Example:
<add key="EnableOData" value="true" />After this you need to restart Vtrin Server service to enable the feature.
A Postman collection was prepared, which is probably the easiest way to check out the API. You can find it in the examples section of this document.
Get started with GET requests
Let us assume a situation, where you are running ABB Ability™ History on your local machine. Hence, Vtrin Server is running in the address http://localhost/. Then our OData service root is http://localhost/odata/. Just by calling the service root, we get a list of all entity types in the server with some basic information about each type. So, making a GET request to http://localhost/odata will return the following kind of JSON response:
{
"@odata.context": "http://localhost/odata/$metadata",
"value": [
...
{
"name": "Path_Pipe",
"kind": "EntitySet",
"url": "Path_Pipe"
},
...
]
}Metadata query option
The $metadata query option will generate a result with metadata about each entity type on the server. Making a GET request to http://localhost/odata/$metadata will return the following kind of XML response:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="ABB.Vtrin.OData" xmlns="http://docs.oasis-open.org/odata/ns/edm">
. . .
<EntityType Name="Path_Pipe" OpenType="true">
<Key>
<PropertyRef Name="Name" />
</Key>
<Property Name="Id" Type="Edm.Guid" Nullable="false" />
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="DisplayName" Type="Edm.String" />
<Property Name="Parent_navigationIdProperty" Type="Edm.String" Nullable="false" />
<Property Name="Equipment_navigationIdProperty" Type="Edm.String" Nullable="false" />
<Property Name="References" Type="Collection(Edm.String)" Nullable="false" />
<Property Name="Manufacturer" Type="Edm.String" />
<Property Name="Diameter" Type="Edm.Double" />
<Property Name="Flow" Type="Edm.Double" />
<NavigationProperty Name="Parent" Type="ABB.Vtrin.OData.Path" />
<NavigationProperty Name="Equipment" Type="ABB.Vtrin.OData.Equipment" />
<Annotation Term="OData.Community.Keys.V1.AlternateKeys">
<Collection>
<Record Type="OData.Community.Keys.V1.AlternateKey">
<PropertyValue Property="Key">
<Collection>
<Record Type="OData.Community.Keys.V1.PropertyRef">
<PropertyValue Property="Alias" String="Id" />
<PropertyValue Property="Name" PropertyPath="Id" />
</Record>
</Collection>
</PropertyValue>
</Record>
</Collection>
</Annotation>
</EntityType>
. . .
</Schema>
</edmx:DataServices>
</edmx:Edmx>Fetching instances
The following examples are based on the Equipment Model that is created in Equipment Model tutorial.
Fetch all instances of an entity and their properties
Endpoint http://localhost/odata/Path_Pipe will return all Path_Pipe entities from the database as shown below. To try with another instance, you may switch the /Path_Pipe to any entity type name you find in the result from http://localhost/odata.
{
"@odata.context": "http://localhost/odata/$metadata#Path_Pipe",
"value": [
{
"@odata.id": "Path_Pipe('Example%20site.Water%20transfer%20system.Flowback%20pipe')",
"@odata.editLink": "Path_Pipe('Example%20site.Water%20transfer%20system.Flowback%20pipe')",
"Id": "bb3784ae-dcfe-434e-8a5b-46dfc322c718",
"Name": "Example site.Water transfer system.Flowback pipe",
"DisplayName": "Flowback pipe",
"References": [],
"Manufacturer": "Pumps & Pipes Inc.",
"Diameter": 10.0,
"Flow": 100.0,
"[email protected]": "Path('Example%20site.Water%20transfer%20system')",
"[email protected]": "Equipment('Pipe')"
},
{
"@odata.id": "Path_Pipe('Example%20site.Water%20transfer%20system.Pipe')",
"@odata.editLink": "Path_Pipe('Example%20site.Water%20transfer%20system.Pipe')",
"Id": "1941ca91-3547-4da6-b404-cc2fc1592971",
"Name": "Example site.Water transfer system.Pipe",
"DisplayName": "Pipe",
"References": [],
"Manufacturer": "Pumps & Pipes Inc.",
"Diameter": 20.0,
"Flow": 0.0,
"[email protected]": "Path('Example%20site.Water%20transfer%20system')",
"[email protected]": "Equipment('Pipe')"
}
]
}Fetch one instance of an entity and its properties
http://localhost/odata/Path_Pipe('Example site.Water transfer system.Pipe')
Often we are not interested in getting all entities, but only one. If we know the type (Path_Pipe) and the name (Example site.Water transfer system.Pipe) of the entity we can call it with this request.
{
"@odata.context": "https://10.58.44.108/odata/$metadata#Path_Pipe/$entity",
"@odata.id": "Path_Pipe('Example%20site.Water%20transfer%20system.Pipe')",
"@odata.editLink": "Path_Pipe('Example%20site.Water%20transfer%20system.Pipe')",
"Id": "1941ca91-3547-4da6-b404-cc2fc1592971",
"Name": "Example site.Water transfer system.Pipe",
"DisplayName": "Pipe",
"References": [],
"Manufacturer": "Pumps & Pipes Inc.",
"Diameter": 20.0,
"Flow": 0.0,
"[email protected]": "Path('Example%20site.Water%20transfer%20system')",
"[email protected]": "Equipment('Pipe')"
}Controlling returned data
System query options
The OData protocol defines several query options that can be used to alter the result of a request. A system query option has a key with the prefix $.
An example with the $filter option: Returning equipment with a nominal power of 150: http[s]://[hostname]/odata/Path?$filter=Nominal_power eq 150
The following query options are implemented in the Vtrin server:
- $filter
- $expand
- $metadata
- $top
- $skip
- $orderby
The following query options are not implemented
- $count
- $search
Client driven paging
Sometimes the client doesn't want to fetch all the data in one go. It is possible to control the size and position of the server response in a call. This is called client driven paging. This may be easily done with $top and $skip query options. These methods are preferred over changing the odata.maxpagesize header.
It is recommended to use client driven paging rather than server driven paging (explained next). This was customer may control the amount of data fetched.
$top and $skip
When the client fetches data from the API, it can use the system query option $top to decide the max size of each collection returned. For example:
- To get the top 100 entries in the result we call:
http[s]://[hostname]/odata/Path?$top=100 - Then we might be interested to get the next 100 entries. Here, we skip the first 100 entries and then fetch the next 100 entries like this: in addition we use $skip make a call:
http[s]://[hostname]/odata/Path?$top=100&$skip=100.
Note to user:
- when using $top query, the server does not indicate if there is more data in each collection. Next section will discuss @odata.nextLink. This is not however added to $top calls, if the $top we are using is smaller than the server configured max size.
- The $top results are picked after $filter, $skip or other alterations are applied.
Server driven paging
If the response contains too many entities, the result will be cut at a limit returning only the results above the limit. A response with cut result will have an additional annotation inside, @odata.nextLink. The value of the nextLink is an URL to fetch the rest of the result. If the remaining result is also too big, a new nextLink will be included for the next page. When there is no more nextLink there is no more data to fetch. This is called server driven paging.
The default max page size is 10 000 results per entity set. This can be changed by the client with the request header odata.maxpagesize. Before altering this header, the client should consider using the $top and $skip instead (discussed above in section Client Driven Paging).
Headers
OData defines several headers. Below we list supported headers in the Vtrin server. The headers applied in a server response can be read in the response header "Preference-Applied".
Request headers
| Key | Value | Description | Example |
|---|---|---|---|
Prefer |
odata.maxpagesize=[none negative integer] |
Define the max page size of used by the server driven paging. This should normally stay unused. Note: The default value is 50 000 |
odata.maxpagesize=500 |
Prefer |
Odata.include-annotations=[regex] |
Define what annotations to include, or exclude, in the response data. Note: The server might ignore this value for certain requests or annotations even if it list it in “Preference-Applied” response header. |
Odata.include-annotations=”*” |
There is 'IEEE754Compatible' property that is also can be included in the Accept Header. The server will write quoted string for INT64 and UINT64 to prevent data loss.
Response headers
| Key | Value | Description | |
|---|---|---|---|
| OData-Version | 4.0 | Define the OData version in use. It should always return 4.0 | OData-Version=4.0 |
| Preference-Applied | [comma separated list or applied request headers with key Prefer] | This header lists all request headers that was applied during the creation of the response result. | Preference-Applied=Odata.maxpagesize=4, Odata.include-annotations=”*” |
Creating instances
To create a new class instance, make a POST request to the endpoint of that class. The body of this request shall contain property values that user wants to assign to the new instance. The following example creates a new instance of Pipe equipment:
POST http://localhost/odata/Path_Pipe
{
"Name": "Factory.Pipes.Drain Pipe",
"Diameter": 15,
"Manufacturer": "Pumps & Pipes Inc."
}In case of success the server will respond with Created (201) and a created instance:
{
"@odata.context": "https://localhost/odata/$metadata#Path_Pipe/$entity",
"@odata.id": "Path_Pipe('Factory.Pipes.Drain%20Pipe')",
"@odata.editLink": "Path_Pipe('Factory.Pipes.Drain%20Pipe')",
"Id": "b79a9fe9-94f8-4e51-b765-60496de8cca5",
"Name": "Factory.Pipes.Drain Pipe",
"DisplayName": "Drain Pipe",
"References": [],
"Diameter": 15.0,
"Flow": 0.0,
"Manufacturer": "Pumps & Pipes Inc.",
"[email protected]": "Path('Factory.Pipes')",
"[email protected]": "Equipment('Pipe')"
}Modifying instances
Modifications of class instances are done through making a PATCH or PUT request to the endpoint of the target class. Similar to creation, the body of this request shall contain updates to property values. The user must specify single or multiple instances to be updated either via their keys or by query filters ($filter, $top and $skip are supported).
Modify a single entity instance
The following example edits the name of previously created pipe:
PATCH http://localhost/odata/Path_Pipe('Factory.Pipes.Drain Pipe')
{
"DisplayName": "Pipe 1"
}In case of success the server will respond with NoContent (204) and a modified instance:
{
"@odata.context": "https://localhost/odata/$metadata#Path_Pipe/$entity",
"@odata.id": "Path_Pipe('Factory.Pipes.Pipe%201')",
"@odata.editLink": "Path_Pipe('Factory.Pipes.Pipe%201')",
"Id": "b79a9fe9-94f8-4e51-b765-60496de8cca5",
"Name": "Factory.Pipes.Pipe 1",
"DisplayName": "Pipe 1",
"References": [],
"Diameter": 15.0,
"Flow": 0.0,
"Manufacturer": "Pumps & Pipes Inc.",
"[email protected]": "Path('Factory.Pipes')",
"[email protected]": "Equipment('Pipe')"
}Modify multiple entity instances
The following example updates the Diameter fields of all pipes under 'Factory.Pipes' node:
PATCH http://localhost/odata/Path_Pipe?$filter=Parent/Name eq 'Factory.Pipes'
{
"Diameter": 12
}In case of success the server will respond with NoContent (204) and all modified instances:
{
"@odata.context": "https://localhost/odata/$metadata#Path_Pipe",
"value": [
{
"@odata.id": "Path_Pipe('Factory.Pipes.Astra')",
"@odata.editLink": "Path_Pipe('Factory.Pipes.Astra')",
"Id": "9f12e4e9-3215-4be8-b9dc-23e139f503b4",
"Name": "Factory.Pipes.Astra",
"DisplayName": "Astra",
"References": [],
"Diameter": 12.0,
"Flow": 0.0,
"Manufacturer": "Seegson",
"[email protected]": "Path('Factory.Pipes')",
"[email protected]": "Equipment('Pipe')"
},
{
"@odata.id": "Path_Pipe('Factory.Pipes.Pipe%201')",
"@odata.editLink": "Path_Pipe('Factory.Pipes.Pipe%201')",
"Id": "b79a9fe9-94f8-4e51-b765-60496de8cca5",
"Name": "Factory.Pipes.Pipe 1",
"DisplayName": "Pipe 1",
"References": [],
"Diameter": 12.0,
"Flow": 0.0,
"Manufacturer": "Pumps & Pipes Inc.",
"[email protected]": "Path('Factory.Pipes')",
"[email protected]": "Equipment('Pipe')"
}
]
}Removing instances
To delete a class instance, make a DELETE request to an endpoint of that class. The user must specify single or multiple instances to be removed either via their keys or by query filters ($filter, $top and $skip are supported).
Remove a single entity instance
The following example removes a specific pipe instance:
DELETE http://localhost/odata/Path_Pipe('Factory.Pumps.Pipe 2')In case of success, the server will respond with NoContent (204).
Remove multiple entity instances
The following example removes all pipes with "Pumps & Pipes Inc." manufacturer:
DELETE http://localhost/odata/Path_Pipe?$filter=Manufacturer eq 'Pumps %26 Pipes Inc.'In case of success, the server will respond with NoContent (204).
Functions
The OData API in Vtrin has a number of custom functions implemented. This section will document them. It is vital to remember that, functions can only read data, not write or modify. Actions to modify and write will be covered after functions.
A function is called with http[s]://[hostname]/odata/[functionname](...parameters) and there are four functions we will go over next:
- GET FetchGraphData(): Simple data fetch for both ProcessHistory and EquipmentHistory.
- GET FetchGraphDataMerge(): Multiple fetches of data and merge the result on one x-axis.
- **GET FetchGraphDataStack(): **Multiple fetches of data and stack the result on one x-axis.
- GET FetchGraphDataCalc(): Multiple fetches of data and do calculations with the result on one x-axis.
GET FetchGraphData()
| Param name | Type | Description |
|---|---|---|
classname |
String |
Required. Name of the class. ProcessHistory = Variables, EquipmentHistory = Equipment Model. Example: |
wherestring |
String |
Required. Specify the conditions for the query. Values for arguments should bemarked with ? and defined in the whereargs parameter. Example: |
whereargs |
Collection(String) |
Required. Specify the variables used in the wherestring parameter. Example: |
endx |
String |
Required. Specify the end value on the x-axis. Example: |
startx |
String |
Specify the start value on the x-axis. By leaving out startx, the system will count backwards from the endx. This will give another result than having startx. Example: |
xproperty |
String |
Specify the x property. Default: Example: |
yproperty |
String |
Specify the y property. Default: Example: |
filter |
String |
Specify the filter. Default: Example: |
yformat |
Enum.RESULT_FORMAT |
Set if the y values should be formatted or not in the response. Accepted values are Default: Example: |
statusformat |
Enum.RETURN_FORMAT |
Set if the status values should be formatted or not in the response. Accepted values are Default: Example: |
Examples
Example with only mandatory parameters for fetching Equipment History:
http://localhost/odata/FetchGraphData(classname='EquipmentHistory', startx='2020-02-01T00:00:00Z', endx='2020-03-01T00:00:00Z', wherestring='Path=%3F AND Property=%3F', whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level'])
Example with all parameters:
http://localhost/odata/FetchGraphData(classname='EquipmentHistory', startx='2020-02-20T00:00:00Z', endx='2020-03-01T00:00:00Z', wherestring='Path=%3F AND Property=%3F', whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level'], xproperty='Time', yproperty='Value', yformat=ABB.Vtrin.OData.RETURN_FORMAT'ALL', statusformat=ABB.Vtrin.OData.RETURN_FORMAT'ALL', filter='AVG30min')
Example for fetching Process History:
http://localhost/odata/FetchGraphData(classname='ProcessHistory', startx='2023-06-06T06:00:00Z', endx='2023-06-07T06:10:00Z', wherestring='Variable=%3F', whereargs=['Variablename'])
GET FetchGraphDataMerge()
If you would like to do multiple fetches of data and merge the result on one x-axis, this function is for you. By calling this function Vtrin will fetch and merge the data for you.
| Param name | Type | Description |
|---|---|---|
classnames |
Collection(String) |
Required. Name of the classes for each query. Example: |
wherestrings |
Collection(String) |
Required. Specify the conditions for each query. Values for arguments should bemarked with ? and defined in the whereargs parameter. Example: |
whereargs |
Collection(String) |
Required. Specify the variables used in the wherestring parameter. Example: |
endx |
String |
Required. Specify the end value on the x-axis. Modify endx if needed. Example: |
startx |
String |
Specify the start value on the x-axis. By leaving out startx, the system will count backwards from the endx. This will give another result than having startx. Modify startx if needed. Example: |
xpropertys |
Collection(String) |
Specify the x property of each query. Default: Example: |
ypropertys |
Collection(String) |
Specify the y property of each query. Default: Example: |
queryfilters |
Collection(String) |
Specify the filter for each query. Default: Example: |
mergefilter |
String |
Specify the filter for the merge. Default: Example: |
yformat |
Enum.RETURN_FORMAT |
Set if the y values should be formatted or not in the response.Accepted values are Default: Example: |
statusformat |
Enum.RETURN_FORMAT |
Set if the status values should be formatted or not in the response.Accepted values are Default: Example: |
interpolate |
Boolean |
Set to FALSE if only real Y values need to be presented. Default: Example: |
combinestatuses |
Boolean |
Set to FALSE if status values should be presented individually for each fetch item. Default: Example: |
Examples
Example with only mandatory parameters:
http://localhost/odata/FetchGraphDataMerge(classnames=['EquipmentHistory', 'EquipmentHistory'], wherestrings=['Path=%3F AND Property=%3F', 'Path=%3F AND Property=%3F'], whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level', 'Example site.Water transfer system.Tank area.Target tank', 'Level'], startx='2020-02-26T11:00:12.459826Z', endx= '2020-02-26T11:00:23.459826Z')
Example with all parameters:
http://localhost/odata/FetchGraphDataMerge(classnames=['EquipmentHistory', 'EquipmentHistory'], wherestrings=['Path=%3F AND Property=%3F', 'Path=%3F AND Property=%3F'], whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level', 'Example site.Water transfer system.Tank area.Target tank', 'Level'], startx='2020-02-26T11:00:12.459826Z', endx= '2020-02-26T11:00:23.459826Z', xpropertys=['Time', 'Time'], ypropertys=['Value', 'Value'], queryfilters=['', ''], mergefilter='', yformat=ABB.Vtrin.OData.RETURN_FORMAT'FORMATED', statusformat=ABB.Vtrin.OData.RETURN_FORMAT'FORMATED')
GET FetchGraphDataStack()
If you would like to do multiple fetches of data and stack the result on one x-axis, this function is for you. By calling this function Vtrin will fetch and stack the data for you.
| Param name | Type | Description |
|---|---|---|
classnames |
Collection(String) |
Required. Name of the classes for each query. Example: |
wherestrings |
Collection(String) |
Required. Specify the conditions for each query. Values for arguments should bemarked with ? and defined in the whereargs parameter. Example: |
whereargs |
Collection(String) |
Required. Specify the variables used in the wherestring parameter. Example: |
endx |
String |
Required. Specify the end value on the x-axis. Example: |
startx |
String |
Specify the start value on the x-axis. By leaving out startx, the system will count backwards from the endx. This will give another result than having startx. Example: |
xpropertys |
Collection(String) |
Specify the x property of each query. Default: Example: |
ypropertys |
Collection(String) |
Specify the y property of each query. Default: Example: |
queryfilters |
Collection(String) |
Specify the filter for each query. Default: Example: |
stackfilter |
String |
Specify the filter for the stack. Default: Example: |
yformat |
Enum.RETURN_FORMAT |
Set if the y values should be formatted or not in the response. Accepted values are Default: Example: |
statusformat |
Enum.RETURN_FORMAT |
Set if the status values should be formatted or not in the response. Accepted values are Default: Example: |
Examples
Example with only mandatory parameters:
http://localhost/odata/FetchGraphDataStack(classnames=['EquipmentHistory', 'EquipmentHistory'], wherestrings=['Path=%3F AND Property=%3F', 'Path=%3F AND Property=%3F'], whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level', 'Example site.Water transfer system.Tank area.Target tank', 'Level'], startx='2020-02-26T11:00:12.459826Z', endx= '2020-02-26T11:00:23.459826Z')
Example with all parameters:
http://localhost/odata/FetchGraphDataStack(classnames=['EquipmentHistory', 'EquipmentHistory'], wherestrings=['Path=%3F AND Property=%3F', 'Path=%3F AND Property=%3F'], whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level', 'Example site.Water transfer system.Tank area.Target tank', 'Level'], startx='2020-02-26T11:00:12.459826Z', endx= '2020-02-26T11:00:23.459826Z', xpropertys=['Time', 'Time'], ypropertys=['Value', 'Value'], queryfilters=['', ''], stackfilter='', yformat=ABB.Vtrin.OData.RETURN_FORMAT'FORMATED', statusformat=ABB.Vtrin.OData.RETURN_FORMAT'FORMATED')
GET FetchGraphDataCalc()
If you would like to do multiple fetches of data and do calculations with the result on one x-axis, this function is for you.
| Param name | Type | Description |
|---|---|---|
classnames |
Collection(String) |
Required. Name of the classes for each query. Example: |
wherestrings |
Collection(String) |
Required. Specify the conditions for each query. Values for arguments should be Example: |
whereargs |
Collection(String) |
Required. Specify the variables used in the wherestring parameter. Example: |
expression |
String |
Required. The expression to calculate. Example: |
expressionIndices |
Collection(String) |
Required. Maps the index of the queries to a variable, that is used in the expression. In the example ['A', 'B'], the result of the query on index 0 in wherestrings will be bind to the variable A, and index 1 will be bound to B. Example: |
endx |
String |
Required. Specify the end value on the x-axis. Example: |
startx |
String |
Specify the start value on the x-axis. By leaving out startx, the system will count backwards from the endx. This will give another result than having startx. Example: |
xpropertys |
Collection(String) |
Specify the x property of each query. Default: Example: |
ypropertys |
Collection(String) |
Specify the y property of each query. Default: Example: |
queryfilters |
Collection(String) |
Specify the filter for each query. Default: Example: |
calcfilter |
String |
Specify the filter for the calculation. Default: Example: |
yformat |
Enum.RETURN_FORMAT |
Set if the y values should be formatted or not in the response. Default: Example: |
statusformat |
Enum.RETURN_FORMAT |
Set if the status values should be formatted or not in the response. Accepted values are Default: Example: |
Examples
Example with only mandatory parameters:
http://localhost/odata/FetchGraphDataCalc(classnames=['EquipmentHistory', 'EquipmentHistory'], wherestrings=['Path=%3F AND Property=%3F', 'Path=%3F AND Property=%3F'], whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level', 'Example site.Water transfer system.Tank area.Target tank', 'Level'], startx='2020-02-26T11:00:12.459826Z', endx= '2020-02-26T11:00:23.459826Z', expression='A+B', expressionIndices=['A', 'B'])
Example with all parameters:
http://localhost/odata/FetchGraphDataCalc(classnames=['EquipmentHistory', 'EquipmentHistory'], wherestrings=['Path=%3F AND Property=%3F', 'Path=%3F AND Property=%3F'], whereargs=['Example site.Water transfer system.Tank area.Source tank', 'Level', 'Example site.Water transfer system.Tank area.Target tank', 'Level'], startx='2020-02-26T11:00:12.459826Z', endx= '2020-02-26T11:00:23.459826Z', expression='A+B', expressionIndices=['A', 'B'], xpropertys=['Time', 'Time'], ypropertys=['Value', 'Value'], queryfilters=['', ''], calcfilter='', yformat=ABB.Vtrin.OData.RETURN_FORMAT'FORMATED', statusformat=ABB.Vtrin.OData.RETURN_FORMAT'FORMATED')
Actions
Actions in OData are a bit broader term than function. A function does not have side effects but actions can have those. In Vtrin we use the term action to describe an operation that writes or modifies data.
Next we will go over two Actions:
- POST WriteGraphData(): Writes an array of specified values to the history
- POST ReplaceGraphDataRange(): Replaces a range in the history with new values.
POST WriteGraphData()
Writes an array of specified values to the history. Deletion of values are also included in this write action, by specifying a null y value. To call this action post a JSON object with the following properties:
| Param name | Type | Description |
|---|---|---|
classname |
Collection(String) |
Required. Name of the class to write history to. Example: |
wherestring |
String |
Required. Specify the conditions for the query. Values for arguments should be Example: |
whereargs |
Collection(String) |
Required. Specify the variables used in the wherestring parameter. Example: |
XValues |
Collection(String) |
Required. The x values of each new value to write. Example: |
YValues |
Collection(String) |
Required. The y values of each new value to write. Example: |
Statuses |
Collection(String) |
Required. The status values of each new value to write. Example: |
xproperty |
String |
Specify the x property. Default: Example: |
yproperty |
String |
Specify the y property. Default: Example: |
Responses
- NoContent (204) - If all values were written without problem.
- OK (200) - If not all values were written. The body will contain information of what values could not be written and why.
Example
This is the URL and body for adding two data points:
POST http://localhost/odata/WriteGraphData()
{
"classname": "EquipmentHistory",
"XValues": ["2020-03-02T13:00:00Z", "2020-03-02T14:00:00Z"],
"YValues": [1500, 2000],
"Statuses": ["OK", "OK"],
"wherestring": "Path=? AND Property=?",
"whereargs": ["Example site.Water transfer system.Tank area.Target tank", "Level"]
}If the point exists the server will respond with NoContent (204). If the point does not exist, the server will respond with OK (200) and a JSON body containing the error. Here is an example response body for trying to remove a data point which does not exist:
{
"@odata.context": "http://localhost/odata/$metadata#Collection(ABB.Vtrin.OData.WriteGraphDataResultType)",
"value": [
{
"xvalue": "26/11/2018 7.34.47",
"result": "Not found"
}
]
}POST ReplaceGraphDataRange()
Replaces a range in the history with new values. All old values in the range specified by startx and endx will be removed. This can also be used to delete ranges in history by not including any new values.
To call this action post a JSON object with the following properties:
| Param name | Type | Description |
|---|---|---|
classname |
Collection(String) |
Required. Name of the class to write history to. Example: |
wherestring |
String |
Required. Specify the conditions for the query. Values for arguments should be Example: |
whereargs |
Collection(String) |
Required. Specify the variables used in the wherestring parameter. Example: |
XValues |
Collection(String) |
Required. The x values of each new value to write. Example: |
YValues |
Collection(String) |
Required. The y values of each new value to write. Example: |
Statuses |
Collection(String) |
Required. The status values of each new value to write. Example: |
startx |
String |
Required. Specify the start x of the range to replace. Example: |
endx |
String |
Required. Specify the start x of the range to replace. Example: |
xproperty |
String |
Specify the x property. Default: Example: |
Responses
- NoContent (204) - If all values were written without problem.
- OK (200) - If not all values were written. The body will contain information of what values could not be written and why.
Example
This is the URL and body of removing everything between 2020-03-02T12:00:00 and 2020-03-02T15:00:00 and adding two data points:
http://localhost/odata/ReplaceGraphDataRange()
{
"classname": "EquipmentHistory",
"startx" : "2020-03-02T12:00:00Z",
"endx" : "2020-03-02T15:00:00Z",
"XValues": ["2020-03-02T12:00:00Z", "2020-03-02T15:00:00Z"],
"YValues": [2500, 3000],
"Statuses": ["OK", "OK"],
"wherestring": "Path=? AND Property=?",
"whereargs": ["Example site.Water transfer system.Tank area.Target tank", "Level"]
}If everything goes right the server will respond with NoContent (204). If some points could not be written the server will respond with OK (200) and a JSON explaining this. In the example below we tried to write a value outside startx and endx.
{
"@odata.context": "http://localhost/odata/$metadata#Collection(ABB.Vtrin.OData.WriteGraphDataResultType)",
"value": [
{
"xvalue": "2016-01-04T10:10:00Z",
"result": "outside range"
}
]
The status value
The status of the value can be defined in two ways.
- Raw long value, as they are stored in the server. Example: 268370432.
- A list using the "|" character to separate the different data in the status value. Example: OK | Representativeness=0.5
The following words in the "|" separated list expect to come with a value after a "=" character:
- Representativeness=1
- AlarmLevel=13
- UserStatus=1
Other words come alone like "OK" or "Fake".
Two special ones is
- "OK" - This is the same as "OK | Representativeness=1"
- "Invalid" - This is the same as "Invalid | Representativeness=0"
Example requests
Equipment Instances
- Combine $select and $expand:
http[s]://[hostname]/odata/Path?$expand=Equipment($select=Name)&$select=Equipment - ** Fetch all equipment instances:**:
http[s]://[hostname]/odata/Path - Fetch only equipment ids and display names:
http[s]://[hostname]/odata/Path?$select=Id,DisplayName - Fetch all equipment instances with their equipment data inline:
http[s]://[hostname]/odata/Path?$expand=Equipment - Fetch all equipment instances, their equipment and the base of equipment, inline:
http[s]://[hostname]/odata/Path?$expand=Equipment($expand=Base) - Fetch only equipment instances whose manufacturer is "Tank Company":
http[s]://[hostname]/odata/Path?$filter=Manufacturer eq 'Tank Company' - Fetch only equipment instances whose Parent is 'Example site1.Water transfer system2':
(filtering with a property of a related OData entity)
http[s]://[hostname]/odata/Path?$filter=Parent/Name eq 'Example site1.Water transfer system2' - Fetch only equipment instances whose parent is "Plant.Section C'' and manufacturer is "Tank Company":
http[s]://[hostname]/odata/Path?$filter=Parent/Name eq 'Plant.Section C' and Manufacturer eq 'Tank Company' - Fetch all equipment instances for class "Pipe":
http[s]://[hostname]/odata/Path_Pipe - Fetch all equipment instances of class Tank whose manufacturer is "Tank Company" or which were installed after a certain date:
http[s]://[hostname]/odata/Path_Tank?$filter=Manufacturer eq 'Tank Company' or InstallationDate gt 2020-06-01T01:00:25Z - Fetch the equipment instance of class "Pipe" with name "Example site.Water transfer system.Pipe":
http[s]://[hostname]/odata/Path_Pipe('Example site.Water transfer system.Pipe')
Equipment Classes
- Fetch all equipment classes:
http[s]://[hostname]/odata/Equipment
Single Equipment
- Fetch single equipment with known name:
http[s]://[hostname]/odata/Equipment('Pump') - Fetch single equipment with known id:
http[s]://[hostname]/odata/Equipment(Id=36077faa-52e2-483e-9c24-a51d6dc74f7a) - Fetch single equipment event with known time and Id (compound ID):
http[s]://[hostname]/odata/EquipmentEvent('{2021-09-08T10:16:47.9484222Z,11512494688535426655}')
Postman Collection:Postman collection can be found here. All endpoints are configured to use Postman variables: cpmPlusUrl, cpmPlusUsername, cpmPlusPassword.
Example programs
Here are short example programs for some basic use cases of the OData API for the Vtrin server. For the "C# Linq" code more advanced modifications have to be done for it to run. The README for it can be found on the following git repository:
<https://tfsa.abb.com/tfs/CPM/cpmPlus/_git/cpmPlus.History.Samples> in the odata/CSharpODataClientExamples folder.
import requests
import json
from requests.exceptions import HTTPError
from datetime import datetime, timedelta
from pathlib import Path
import pprint
# Some functions representing examples for basic use cases of the Vtrin server OData API in Python. It uses an HTTPS connection, so the Vtrin server has to be configured to allow
# plain text passwords.
# Also some TODO:s in this code have to be modified for this to work.
class ConnectionInformation():
def __init__(self,
session,
address):
self.session = session
self.address = address
def WriteGraphData(connectionInfo):
print();
print("================");
print("Starting write graph data example");
linkToServer = f"https://{connectionInfo.address}/odata/WriteGraphData()"
# First writing one datapoint
data = {
"classname": "EquipmentHistory",
# TODO: Replace dates below with dates (Eg. current date) to where data can be written
"XValues": ["2019-05-26T11:00:00Z"],
"YValues": ["5"],
"Statuses": ["OK"],
"wherestring": "Path=? AND Property=?",
# TODO: Replacce TESTSERVER with your computername
"whereargs": ["ABB.System Monitoring.TESTSERVER.Disks.C:", "Load"]
}
# Converting to JSON
data = json.dumps(data)
try:
headers = {"Content-Type": "application/json"}
response = connectionInfo.session.post(linkToServer, data=data, headers=headers)
# If the response was successful, no exception will be raised
response.raise_for_status()
except requests.HTTPError as e:
print(f'https error occurred: {e}')
else:
pprint.pprint(f"Write was successful! {response}")
def FetchGraphData(connectionInfo):
print();
print("================");
print("Starting fetch graph data example");
linkToServer = f"https://{connectionInfo.address}/odata/"
# Creating the data
className = "EquipmentHistory";
xProperty = "Time";
yProperty = "Value";
filter = "";
# TODO: Replace dates below with dates (Eg. current date) from where data can be found
startx = "2018-09-01T00:00:00Z";
endx = "2018-09-06T00:00:00Z";
whereString = "Path=%3F AND Property=%3F";
# TODO: Replacce TESTSERVER with your computername
whereStringArgs = ["ABB.System Monitoring.TESTSERVER.Disks.C:", "Load"];
xPixels = 0
yPixels = 0
# Building query
query = linkToServer + f"FetchGraphData(classname='{className}', xproperty='{xProperty}', yproperty='{yProperty}', startx='{startx}', endx='{endx}', wherestring='{whereString}', whereargs={whereStringArgs})"
try:
response = connectionInfo.session.get(query)
# If the response was successful, no exception will be raised
response.raise_for_status()
except requests.HTTPError as e:
print(f'https error occurred: {e}')
else:
# Fetches X values from response
xValues = json.loads(response.content)['value'][0]['XValues']
pprint.pprint(f"X values: {xValues.__len__()}")
def IterateAndFilterExample(connectionInfo):
print();
print("================");
print("Starting iterate and filter example");
linkToServer = f"https://{connectionInfo.address}/odata/"
query = linkToServer + f"UIUnit?$filter=Id eq 'Ah'"
try:
response = connectionInfo.session.get(query)
# If the response was successful, no exception will be raised
response.raise_for_status()
except requests.HTTPError as e:
print(f'https error occurred: {e}')
else:
pprint.pprint(json.loads(response.content))
def ClientDrivenPagingExample(connectionInfo):
print();
print("================");
print("Starting client driven paging example");
linkToServer = f"https://{connectionInfo.address}/odata/"
query = linkToServer + f"UIUnit?$skip=1&$top=1"
try:
response = connectionInfo.session.get(query)
# If the response was successful, no exception will be raised
response.raise_for_status()
except requests.HTTPError as e:
print(f'https error occurred: {e}')
else:
pprint.pprint(json.loads(response.content))
def OrderByExample(connectionInfo):
print();
print("================");
print("Starting order by example");
linkToServer = f"https://{connectionInfo.address}/odata/"
query = linkToServer + f"Variable?$top=5&$orderby=Name desc"
try:
response = connectionInfo.session.get(query)
# If the response was successful, no exception will be raised
response.raise_for_status()
except requests.HTTPError as e:
print(f'https error occurred: {e}')
else:
pprint.pprint(json.loads(response.content))
def main():
# Related to the connection and authentication
print("Enter server ip-address:")
address = input()
print(f"Enter username for {address}:")
username = input()
print("Enter password:")
password = input()
# A session with which https requests will be made
session = requests.Session()
# Creating a object containing information about connection
connectionInfo = ConnectionInformation(session, address)
# Adding uathentication to session
connectionInfo.session.auth = (username, password)
# For own servers verification is recommended
# TODO: Change certificate path to server certificate path
path_to_cert = Path("C:\\PATH TO CERT\\CERT.crt")
connectionInfo.session.verify = path_to_cert
# Test functions
WriteGraphData(connectionInfo)
FetchGraphData(connectionInfo)
IterateAndFilterExample(connectionInfo)
ClientDrivenPagingExample(connectionInfo)
OrderByExample(connectionInfo)
if __name__ == "__main__":
main()#Install packages
#Install packages
install.packages('httr')
install.packages('rjson')
# The code is best run in R studio, as it seems to have access to Windows Certificate Store which helps in allowing HTTPS connections
library('httr')
library('rjson')
username = "myusername"
password = "mypassword"
address = "https://testserver/odata/"
# Function performing the fetching
read_info <- function(url){
response <- GET(url, authenticate(username, password, type="basic"))
return(content(response, "text"))
}
# Converts request response to JSON format
result_to_json <- function(string){
return(fromJSON(string, flatten = TRUE))
}
# Converts JSON to a data frame
json_to_df <- function(json){
return(as.data.frame(json))
}
# Function that concatenates the complete address
complete_address <- function(query){
return(paste(address, query, sep=""))
}
# Iterate and filter example (spaces have to be written in hexadecimal i.e. %20)
filtered_data <- read_info(complete_address("UIUnit?$filter=Id%20eq%20'Ah'"))
filtered_data_json <- result_to_json(filtered_data)
# Order-by example
ordered_data <- read_info(complete_address("Variable?$top=100&$orderby=Name desc"))
ordered_data_json <- result_to_json(ordered_data)
ordered_data_df <- json_to_df(ordered_data_json)
# Client driven paging example
paged_data <- read_info(complete_address("UIUnit?$skip=1&$top=1"))
paged_data_json <- result_to_json(paged_data)
paged_data_df <- json_to_df(paged_data_json)
# Fetching historical data from the database
historical_data <- read_info(complete_address('FetchGraphData(classname=\'ProcessHistory\',wherestring=\'Path=%3F%20AND%20Property=%3F\',whereargs=[\'ABB.System Monitoring.TESTSERVER.Disks.C:\',\'Load\'],endx=\'2019-12-27T18:59:00Z\')'))
historical_data_json <- result_to_json(historical_data)
# Inserting all relevant values to a data frame
x <- historical_data_json['value'][[1]]['XValues']
y <- historical_data_json['value'][[1]]['YValues']
s <- historical_data_json['value'][[1]]['Statuses']
historical_data_df <- data.frame(x[[1]], y[[1]], s[[1]])
names(historical_data_df)[1] <- "X Values"
names(historical_data_df)[2] <- "Y values"
names(historical_data_df)[3] <- "Statuses"using ABB.Vtrin.OData;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Formatting;
using System.Xml;
using Newtonsoft.Json;
/// <summary>
/// Some functions representing examples for basic use cases of the Vtrin server OData API in C#. It uses an HTTPS connection, so the Vtrin server has to be configured to allow
/// plain text passwords.
/// Also some TODO:s in this code have to be modified for this to work.
/// </summary>
namespace ABBODataExamples
{
class Example
{
// This method shows how we use the generated proxy class to iterate and filter with the OData system query option $filter, which is mapped to LINQ Where.
public static void IterateAndFilterExample(string address)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting iterate and filter example");
string query = address + "/UIUnit?$filter=Id eq 'Ah'";
Connection.GetContentAsync(query).GetAwaiter().GetResult();
}
// This method shows how we use the generated proxy class to order with the OData system query option $orderby, which is mapped to LINQ Orderby.
public static void OrderByExample(string address)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting order by example");
string query = address + "UIUnit?$skip=1&$top=1";
Connection.GetContentAsync(query).GetAwaiter().GetResult();
}
// This method shows how we use the generated proxy class to do client driven paging with the OData system query options $skip and $top, which is mapped to LINQ Skip and Take.
public static void ClientDrivenPagingExample(string address)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting client driven paging example");
string query = address + "Variable?$top=5&$orderby=Name desc";
Connection.GetContentAsync(query).GetAwaiter().GetResult();
}
public static void FetchGraphDataExample(string address)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting fetch graph data example");
// Creating the data
string className = "EquipmentHistory";
string xProperty = "Time";
string yProperty = "Value";
// TODO: Replace dates below with dates (Eg. current date) to where data can be found
string startx = "2019-06-10T00:00:00Z";
string endx = "2019-06-18T00:00:00Z";
string whereString = "Path=%3F AND Property=%3F";
// TODO: Replacce TESTSERVER with your computername
string whereStringArgs = "['ABB.System Monitoring.TESTSERVER.Disks.C:', 'Load']";
string query = address + "FetchGraphData(classname='"+ className + "', xproperty='" + xProperty + "', yproperty='" + yProperty + "', startx='" + startx + "', endx='" + endx + "', wherestring='" + whereString + "', whereargs=" + whereStringArgs.ToString() + ")";
Connection.GetContentAsync(query).GetAwaiter().GetResult();
}
}
class Connection
{
public static System.String username;
public static System.String password;
public static async Task GetContentAsync(string address)
{
try
{
// Starting the connection and inputting necessary information
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(address);
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
String encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password));
httpClient.DefaultRequestHeaders.Add("Authorization", "Basic " + encoded);
HttpResponseMessage response = await httpClient.GetAsync(httpClient.BaseAddress);
if (response.IsSuccessStatusCode)
{
// Fetching content and converting to Json
string result = await response.Content.ReadAsStringAsync();
var json = JsonConvert.DeserializeObject<object>(result);
Console.WriteLine(json);
}
else
{
Console.WriteLine("Connection status code " + response.StatusCode);
}
}
catch
{
Console.WriteLine("Connection error!");
}
}
}
class LowerLevelExampleForWiki
{
public static void Main(string[] args)
{
Console.WriteLine("Enter server ip - address:");
System.String ip = Console.ReadLine();
string address = "https://" + ip + "/odata";
Console.WriteLine("Enter username for " + address + ":");
Connection.username = Console.ReadLine();
Console.WriteLine("Enter password:");
Connection.password = Console.ReadLine();
Example.IterateAndFilterExample(address);
Example.OrderByExample(address);
Example.ClientDrivenPagingExample(address);
Example.FetchGraphDataExample(address);
Console.ReadLine();
}
}
}//See why deprecated from AHL-936
using ABB.Vtrin.OData;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// Some functions representing examples for basic use cases of the Vtrin server OData API in C# using the Linq library. It uses an HTTPS connection, so the Vtrin server has to be configured to allow
/// plain text passwords.
/// Also some TODO:s in this code have to be modified for this to work.
///
/// This example requires OData v4 Client Code Generator from https://marketplace.visualstudio.com/items?itemName=bingl.ODatav4ClientCodeGenerator. It is only available for Visual Studio 2015 and 2017.
/// </summary>
namespace ABBODataExamples
{
class Example
{
// This method shows how we use the generated proxy class to iterate and filter with the OData system query option $filter, which is mapped to LINQ Where.
public static void IterateAndFilterExample(DefaultContainer container)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting iterate and filter example");
// Prepearing fetch
var uiUnitQuery = container.UIUnit;
// Server filtering using the query option $filter. This is done by using Linq's Where method.
var filteredUnits = uiUnitQuery.Where(unit => unit.Id.Equals("Ah"));
foreach(UIUnit unit in filteredUnits) Console.WriteLine("Filtering with method 1: " + unit.LongName);
// It can also be done this way
filteredUnits = from unit in uiUnitQuery
where unit.Id.Equals("Ah")
select unit;
foreach (UIUnit unit in filteredUnits) Console.WriteLine("Filtering with method 2: " + unit.LongName);
}
// This method shows how we use the generated proxy class to order with the OData system query option $orderby, which is mapped to LINQ Orderby.
public static void OrderByExample(DefaultContainer container)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting order by example");
// Unordered as reference
var unOrderedPaths = container.Path;
// order with this ...
var orderedPaths = container.Path.OrderByDescending(path => path.DisplayName);
Console.Write("Unordered: "); foreach (Path path in unOrderedPaths.Take(5)) Console.Write(" " + path.DisplayName + ","); Console.WriteLine("...");
Console.Write("Ordered: "); foreach (Path path in orderedPaths.Take(5)) Console.Write(" " + path.DisplayName + ","); Console.WriteLine("...");
}
// This method shows how we use the generated proxy class to do client driven paging with the OData system query options $skip and $top, which is mapped to LINQ Skip and Take.
public static void ClientDrivenPagingExample(DefaultContainer container)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting client driven paging example");
// Get the first unit like this ...
var firstUnit = (from unit in container.UIUnit
select unit).Take(1);
foreach (UIUnit unit in firstUnit) Console.WriteLine("Here we only take the first unit: " + unit.LongName);
// Another way of getting a unit ...
var secondUnit = container.UIUnit.Skip(1).Take(1);
foreach (UIUnit unit in secondUnit) Console.WriteLine("Here we only take the second unit: " + unit.LongName);
}
public static void FetchGraphDataExample(DefaultContainer container)
{
Console.WriteLine();
Console.WriteLine("================");
Console.WriteLine("Starting fetch graph data example");
// Creating the data
string className = "EquipmentHistory";
string xProperty = "Time";
string yproperty = "Value";
string filter = "";
// TODO: Replace dates below with dates (Eg. current date) to where data can be found
string startx = "2019-06-10T00:00:00Z";
string endx = "2019-06-18T00:00:00Z";
string whereString = "Path=%3F AND Property=%3F";
// TODO: Replacce TESTSERVER with your computername
string[] whereStringArgs = { "ABB.System Monitoring.TESTSERVER.Disks.C:", "Load" };
var graphDataQuery = container.FetchGraphData(className, whereString, whereStringArgs, endx, startx, xProperty, yproperty, filter, RETURN_FORMAT.ALL, RETURN_FORMAT.ALL);
// Extracting the values from response
var result = graphDataQuery.GetValue();
Console.Write("X values: "); foreach(var x in result.XValues.Take(5)) Console.Write( " " + x.TimeOfDay + ","); Console.WriteLine("...");
Console.Write("Y values: "); foreach (var y in result.YValues.Take(5)) Console.Write(" " + y + ","); Console.WriteLine("...");
Console.Write("Status values formated: "); foreach (var s in result.StatusesFormated.Take(5)) Console.Write(" " + s + ","); Console.Write("...");
}
}
class Program {
public static void Main(string[] args)
{
DefaultContainer container = new DefaultContainer(new Uri("http://testserver/odata/"));
Example.IterateAndFilterExample(container);
Example.OrderByExample(container);
Example.ClientDrivenPagingExample(container);
Example.FetchGraphDataExample(container);
Console.ReadLine();
}
}
}Power BI reports with OData
This section provides some examples of using OData API in Power BI reporting.
Editing PowerQuery: When creating a report where you have to edit PowerQuery statements, it might be useful to have a secondary report where you test/edit your PowerQuery code.
Power Query Syntax
let
code_line_1 remember to end a statement with a comma
code_line_2 remember to end a statement with a comma
etc no comma at the last line
in
the_final_outputExample Query
First, define some basic variables:
let
Q_START = "'",
Q_END = "'",
token_comma = ",",
token_NOTHING = "",Create variable 'classnames', which is used to specify the individual classes for each variable:
// all 'classnames' point to 'ProcessHistory'
classnames = "["
& Q_START & "ProcessHistory" & Q_END & token_comma /* 1 */
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma /* 5 */
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma
& Q_START & "ProcessHistory" & Q_END & token_comma /* 10 */
& Q_START & "ProcessHistory" & Q_END
& "]",Create variable for start date:
// START: 2021-01-24
startx = Q_START & "2021-01-12T06:00:00Z" & Q_END ,Create variable for end date:
// END: 2021-01-25
endx = Q_START & "2021-01-29T19:00:00Z" & Q_END ,Create a variable for WHERE conditions (for each variable):
// filter by Variable and History
wherestrings = "["
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma /* 1 */
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma /* 5 */
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma
& Q_START & "Variable=%3F AND History=%3F" & Q_END & token_comma /* 10 */
& Q_START & "Variable=%3F AND History=%3F" & Q_END
& "]",Create a variable for arguments: this includes the variable names and sources:
// Variables=SYS_CPU0_Time,SYS_CPU2_Time,SYS_CPU3_Time History is 'CurrentHistory'
whereargs = "["
& "'SYS_CPU0_Time','CurrentHistory'" & token_comma /* 1 */
& "'SYS_CPU2_Time','CurrentHistory'" & token_comma
& "'SYS_CPU3_Time','CurrentHistory'" & token_comma
& "'SYS_CPU_TotalTime','CurrentHistory'" & token_comma
& "'SYS_CPU_TotalTime_OPC','CurrentHistory'" & token_comma /* 5 */
& "'SYS_CVMC_CPU','CurrentHistory'" & token_comma
& "'SYS_CVMC_CPU_OPC','CurrentHistory'" & token_comma
& "'SYS_EcCtsServer_CPU','CurrentHistory'" & token_comma
& "'SYS_NETSERVER_CPU','CurrentHistory'" & token_comma
& "'SYS_TRANSFORMATOR_CPU','CurrentHistory'" & token_comma /* 10 */
& "'SYS_VTRIN_CPU','CurrentHistory'"
& "]",Create a variable for X values (for all variables):
// 'Time' for all variables
xpropertys = "["
& Q_START & "Time" & Q_END & token_comma /* 1 */
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma /* 5 */
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma
& Q_START & "Time" & Q_END & token_comma /* 10 */
& Q_START & "Time" & Q_END
& "]",Create a variable for Y values (for all variables):
// 'Value' for all variables
ypropertys = "["
& Q_START & "Value" & Q_END & token_comma /* 1 */
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma /* 5 */
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma
& Q_START & "Value" & Q_END & token_comma /* 10 */
& Q_START & "Value" & Q_END
& "]",Create a variable for filters for every variable:
// Apply filter 'AVG1hour' to all variables
queryfilters= "["
& Q_START & "AVG1hour" & Q_END & token_comma /* 1 */
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma /* 5 */
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma
& Q_START & "AVG1hour" & Q_END & token_comma /* 10 */
& Q_START & "AVG1hour" & Q_END
& "]",
// yformat is FORMATED (FORMATED,UNFORMATED,ALL)
yformat = "ABB.Vtrin.OData.RETURN_FORMAT'FORMATED'" ,
// statusformat is FORMATED (FORMATED,UNFORMATED,ALL)
statusformat = "ABB.Vtrin.OData.RETURN_FORMAT'FORMATED'",
// empty
FINAL_FILTER = "",
// data is read from "hepsu.ankkalinna.fi.abb.com"
site_address = "https://hepsu.ankkalinna.fi.abb.com/odata",
relative_path = "/" & "FetchGraphDataMerge" & "("
& "classnames=" & classnames
& token_comma
& "startx=" & startx
& token_comma
& "endx=" & endx
& token_comma
& "wherestrings=" & wherestrings
& token_comma
& "whereargs=" & whereargs
& token_comma
& "xpropertys=" & xpropertys
& token_comma
& "ypropertys=" & ypropertys
& token_comma
& "queryfilters=" & queryfilters
& token_comma
& "yformat=" & yformat
& token_comma
& "statusformat=" & statusformat
& ")",Fetch Data
NOTE: **OData.Feed **will not work when you publish your report (it will work in Desktop, though).
Use Web.Contents instead.
Now, fetch data:
//
source = Web.Contents(site_address,
[
RelativePath = relative_path
]
),
Json_output = Json.Document(source),Some initial work:
#"Converted to Table" = Record.ToTable(Json_output),
Value = #"Converted to Table"{1}[Value],
Value1 = Value{0},
// ------------------
XValues = Value1[XValues],
YValuesFormated = Value1[YValuesFormated],
StatusesFormated = Value1[StatusesFormated],The 'Y values' are in a list of lists. The Y values need to be handled separately from 'X values' (time). At the end, when the values are in separate columns, a column named 'index' is added to table. Later on, this 'index' column is used to join 'Y values table' and 'X values table'.
// ------------------
#"yValuesDecimalSeparator" = List.Transform(#"YValuesFormated",each Text.Replace(_,",",".")),
#"yValuesListSeparator" = List.Transform(#"yValuesDecimalSeparator", each Text.Replace(_,";",",")),
#"yValues1" = List.Transform(#"yValuesListSeparator", each Text.Replace(_,"{","")),
#"yValues2" = List.Transform(#"yValues1", each Text.Replace(_,"}","")),
#"yTableInitial" = Table.FromList(#"yValues2"),
#"yTableIndex" = Table.AddIndexColumn(#"yTableInitial", "Index", 1, 1),
#"yTableOrdered" = Table.ReorderColumns(#"yTableIndex",{
"Index",
"Column1", "Column2", "Column3", "Column4", "Column5", "Column6", "Column7", "Column8", "Column9", "Column10",
"Column11"
}),
#"yTableRenamedCols" = Table.RenameColumns(#"yTableOrdered",{
{"Column1", "CPU_0"},
{"Column2", "CPU_2"},
{"Column3", "CPU_3"},
{"Column4", "CPU_TotalTime"},
{"Column5", "SYS_CPU_TotalTime_OPC"},
{"Column6", "SYS_CVMC_CPU"},
{"Column7", "SYS_CVMC_CPU_OPC"},
{"Column8", "SYS_EcCtsServer_CPU"},
{"Column9", "SYS_NETSERVER_CPU"},
{"Column10", "SYS_TRANSFORMATOR_CPU"},
{"Column11", "SYS_VTRIN_CPU"}
}),Handling of 'X values' (time) is simpler than 'Y values':
// --------------------
#"xTable1" = Table.FromList(#"XValues", Splitter.SplitByNothing(), null, null, ExtraValues.Error),
#"xTable2" = Table.AddColumn(#"xTable1","KEY_Time_UTC", each [Column1]),
// Note field KEY_Time_UTC. It is a copy of the original 'Time' value before 'datetime' conversion of applied to 'Time'.
// You will need KEY_Time_UTC, if you wish to combine multiple ODATA queries.
// You can not use 'Time' for joining: 'Time' field will eventually contain duplicate values, due to Daylight Saving Time.
#"xTableInitial" = Table.TransformColumnTypes(#"xTable2",{{"Column1", type datetime}}),
#"xTableIndex" = Table.AddIndexColumn(#"xTableInitial", "Index", 1, 1),
#"xTableOrdered" = Table.ReorderColumns(#"xTableIndex",{"Index", "Column1"}),
#"xTableRenamedCols" = Table.RenameColumns(#"xTableOrdered",{{"Column1", "Time"}}),Now, both tables have a column named 'Index', which is a rownumber. This column is used as a key to join the two tables:
// ----------------------------------------------------
#"JoinedDataTable" = Table.Join(#"xTableRenamedCols","Index", #"yTableRenamedCols", "Index"),The joined table contains a column named 'Index' (note: only one column). Remove 'Index', and you get the final result:
// ----------------------------------------------------
Output = Table.RemoveColumns(#"JoinedDataTable",{"Index"}) /* 'Index' column is not needed anymore */At the end, present the final outcome (this is the value that is returned):
in
OutputUpdated 5 months ago
