Skip to content

Commit

Permalink
[items] ItemPersistence: Add support for persisting TimeSeries (#341)
Browse files Browse the repository at this point in the history
This adds support for persisting a TimeSeries.

It adds a new JS TimeSeries class, which implements the same
functionality as the Java TimeSeries, but in pure JS.
When this JS TimeSeries is passed to `ItemPersistence#persist`, a Java
TimeSeries is created from it by iterating over the timestamp -> state
pairs and using `org.openhab.core.types.TypeParser` to parse Java
`States` from the JS types.

The helpers have been extended with a isInstant and isTimeSeries method
and for all isXXX methods, a check whether the passed in type is an
object has been added.

---------

Signed-off-by: Florian Hotze <[email protected]>
  • Loading branch information
florian-h05 authored Jun 8, 2024
1 parent 076def1 commit 2791284
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 45 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ Calling `Item.persistence` returns an `ItemPersistence` object with the followin
- .persist(serviceId): Tells the persistence service to store the current Item state, which is then done asynchronously.
**Warning:** This has the side effect, that if the Item state changes shortly after `.persist` has been called, the new Item state will be persisted. See [JSDoc](https://openhab.github.io/openhab-js/items.ItemPersistence.html#persist) for a possible work-around.
- .persist(timestamp, state, serviceId): Tells the persistence service to store the given state at the given timestamp, which is then done asynchronously.
- .persist(timeSeries, serviceId): Tells the persistence service to store the given [`TimeSeries`](#timeseries), which is then done asynchronously.
- .persistedState(timestamp, serviceId) ⇒ `PersistedItem | null`
- .previousState(skipEqual, serviceId) ⇒ `PersistedItem | null`
- .nextState(skipEqual, serviceId) ⇒ `PersistedItem | null`
Expand Down Expand Up @@ -569,6 +570,35 @@ console.log('KitchenDimmer maximum was ', historic.state, ' at ', historic.times

See [openhab-js : ItemPersistence](https://openhab.github.io/openhab-js/items.ItemPersistence.html) for full API documentation.

#### `TimeSeries`

A `TimeSeries` is used to transport a set of states together with their timestamp.
It is usually used for persisting historic state or forecasts in a persistence service by using [`ItemPersistence.persist`](#itempersistence).

When creating a new `TimeSeries`, a policy must be chosen - it defines how the `TimeSeries` is persisted in a persistence service:

- `ADD` adds the content to the persistence, well suited for persisting historic data.
- `REPLACE` first removes all persisted elements in the timespan given by begin and end of the `TimeSeries`, well suited for persisting forecasts.

A `TimeSeries` object has the following properties and methods:

- `policy`: The persistence policy, either `ADD` or `REPLACE`.
- `begin`: Timestamp of the first element of the `TimeSeries`.
- `end`: Timestamp of the last element of the `TimeSeries`.
- `size`: Number of elements in the `TimeSeries`.
- `states`: States of the `TimeSeries` together with their timestamp and sorted by their timestamps.
Be aware that this returns a reference to the internal state array, so changes to the array will affect the `TimeSeries`.
- `add(timestamp, state)`: Add a given state to the `TimeSeries` at the given timestamp.

The following example shows how to create a `TimeSeries`:

```javascript
var timeSeries = new items.TimeSeries('ADD'); // Create a new TimeSeries with policy ADD
timeSeries.add(now.minusMinutes(10), Quantity('5 m')).add(now.minusMinutes(5), Quantity('3 m')).add(now.minusMinutes(1), Quantity('1 m'));
console.log(ts); // Let's have a look at the TimeSeries
items.getItem('MyDistanceItem').persistence.persist(timeSeries, 'influxdb'); // Persist the TimeSeries for the Item 'MyDistanceItem' using the InfluxDB persistence service
```

### Things

The Things namespace allows to interact with openHAB Things.
Expand Down
60 changes: 51 additions & 9 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
const utils = require('./utils');
const javaZDT = Java.type('java.time.ZonedDateTime');
const javaDuration = Java.type('java.time.Duration');
const javaInstant = Java.type('java.time.Instant');
const javaTimeSeries = Java.type('org.openhab.core.types.TimeSeries');

/**
* @typedef { import("./items/items").Item } Item
Expand Down Expand Up @@ -50,42 +52,45 @@ function _toOpenhabPrimitiveType (value) {
/**
* Checks whether the given object is an instance of {@link items.Item}.
*
* To be used when instanceof checks don't work because of circular dependencies.
* To be used when instanceof checks don't work because of circular dependencies or webpack compilation.
* Checks constructor name or unique properties, because constructor name does not work for the webpacked globals injection.
*
* @param o {*}
* @param {*} o
* @returns {boolean}
* @private
*/
function _isItem (o) {
if (typeof o !== 'object') return false;
return ((o.constructor && o.constructor.name === 'Item') || typeof o.rawItem === 'object');
}

/**
* Checks whether the given object is an instance of {@link Quantity}.
*
* To be used when instanceof checks don't work because of circular dependencies.
* To be used when instanceof checks don't work because of circular dependencies or webpack compilation.
* Checks constructor name or unique properties, because constructor name does not work for the webpacked globals injection.
*
* @param o {*}
* @param {*} o
* @returns {boolean}
* @private
*/
function _isQuantity (o) {
if (typeof o !== 'object') return false;
return ((o.constructor && o.constructor.name === 'Quantity') || typeof o.rawQtyType === 'object');
}

/**
* Checks whether the given object is an instance of {@link time.ZonedDateTime}.
*
* To be used when instanceof checks don't work because of circular dependencies.
* To be used when instanceof checks don't work because of circular dependencies or webpack compilation.
* Checks constructor name or unique properties, because constructor name does not work for the webpacked globals injection.
*
* @param o {*}
* @param {*} o
* @returns {boolean}
* @private
*/
function _isZonedDateTime (o) {
if (typeof o !== 'object') return false;
return (((o.constructor && o.constructor.name === 'ZonedDateTime')) ||
(!utils.isJsInstanceOfJavaType(o, javaZDT) && typeof o.withFixedOffsetZone === 'function')
);
Expand All @@ -94,24 +99,61 @@ function _isZonedDateTime (o) {
/**
* Checks whether the given object is an instance of {@link time.Duration}.
*
* To be used when instanceof checks don't work because of circular dependencies.
* To be used when instanceof checks don't work because of circular dependencies or webpack compilation.
* Checks constructor name or unique properties, because constructor name does not work for the webpacked globals injection.
*
* @param o {*}
* @param {*} o
* @returns {boolean}
* @private
*/
function _isDuration (o) {
if (typeof o !== 'object') return false;
return (((o.constructor && o.constructor.name === 'Duration')) ||
(!utils.isJsInstanceOfJavaType(o, javaDuration) && typeof o.minusDuration === 'function' && typeof o.toNanos === 'function')
);
}

/**
* Checks whether the given object is an instance of {@link time.Instant}.
*
* To be used when instanceof checks don't work because of circular dependencies or webpack compilation.
* Checks constructor name or unique properties, because constructor name does not work for the webpacked globals injection
*
* @param {*} o
* @returns {boolean}
* @private
*/
function _isInstant (o) {
if (typeof o !== 'object') return false;
return (((o.constructor && o.constructor.name === 'Instant')) ||
(!utils.isJsInstanceOfJavaType(o, javaInstant) && typeof o.policy === 'string' && typeof o.ofEpochMicro === 'function' && typeof o.ofEpochMilli === 'function' && o.ofEpochSecond === 'function')
);
}

/**
* Checks whether the given object is an instance of {@link items.TimeSeries}.
*
* To be used when instanceof checks don't work because of circular dependencies or webpack compilation.
* Checks constructor name or unique properties, because constructor name does not work for the webpacked globals injection.
*
* @param {*} o
* @return {boolean}
* @private
*/
function _isTimeSeries (o) {
if (typeof o !== 'object') return false;
return (((o.constructor && o.constructor.name === 'TimeSeries')) ||
(!utils.isJsInstanceOfJavaType(o, javaTimeSeries) && _isInstant(o.begin) && _isInstant(o.end))
);
}

module.exports = {
_getItemName,
_toOpenhabPrimitiveType,
_isItem,
_isQuantity,
_isZonedDateTime,
_isDuration
_isDuration,
_isInstant,
_isTimeSeries
};
104 changes: 92 additions & 12 deletions src/items/item-persistence.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
const log = require('../log')('items.ItemPersistence');
const time = require('../time');
const utils = require('../utils');
const { getQuantity, QuantityError } = require('../quantity');
const { _toOpenhabPrimitiveType, _isTimeSeries } = require('../helpers');

const PersistenceExtensions = Java.type('org.openhab.core.persistence.extensions.PersistenceExtensions');
const TimeSeries = Java.type('org.openhab.core.types.TimeSeries');
const TypeParser = Java.type('org.openhab.core.types.TypeParser');

/**
* @typedef {import('@js-joda/core').ZonedDateTime} time.ZonedDateTime
* @private
*/
const time = require('../time');
const utils = require('../utils');
/**
* @typedef {import('../quantity').Quantity} Quantity
* @private
*/
const { getQuantity, QuantityError } = require('../quantity');
const { _toOpenhabPrimitiveType } = require('../helpers');
const PersistenceExtensions = Java.type('org.openhab.core.persistence.extensions.PersistenceExtensions');
/**
* @typedef {import('../items/items').TimeSeries} items.TimeSeries
* @private
*/

/**
* Class representing an instance of {@link https://www.openhab.org/javadoc/latest/org/openhab/core/types/state org.openhab.core.types.State}.
Expand Down Expand Up @@ -138,7 +147,7 @@ class ItemPersistence {
/**
* Persists a state of a given Item.
*
* There are four ways to use this method:
* There are six ways to use this method:
* ```js
* // Tell persistence to store the current Item state
* items.MyItem.persistence.persist();
Expand All @@ -147,6 +156,10 @@ class ItemPersistence {
* // Tell persistence to store the state 'ON' at 2021-01-01 00:00:00
* items.MyItem.persistence.persist(time.toZDT('2021-01-01T00:00:00'), 'ON');
* items.MyItem.persistence.persist(time.toZDT('2021-01-01T00:00:00'), 'ON', 'influxdb'); // using the InfluxDB persistence service
*
* // Tell persistence to store a TimeSeries
* items.MyItem.persistence.persist(timeSeries);
* items.MyItem.persistence.persist(timeSeries, 'influxdb'); // using the InfluxDB persistence service
* ```
*
* **Note:** The persistence service will store the state asynchronously in the background, this method will return immediately.
Expand All @@ -160,25 +173,92 @@ class ItemPersistence {
*
* @param {(time.ZonedDateTime | Date)} [timestamp] the date for the item state to be stored
* @param {string|number|time.ZonedDateTime|Quantity|HostState} [state] the state to be stored
* @param {items.TimeSeries} [timeSeries] optional TimeSeries to be stored
* @param {string} [serviceId] optional persistence service ID, if omitted, the default persistence service will be used
*/
persist (timestamp, state, serviceId) {
persist (timestamp, state, timeSeries, serviceId) {
switch (arguments.length) {
// persist a given state at a given timestamp
// persist the current state
case 0:
this.#persistCurrentState();
break;
// persist the current state in a given service or persist a TimeSeries
case 1:
if (_isTimeSeries(arguments[0])) {
this.#persistTimeSeries(arguments[0]);
break;
}
this.#persistCurrentState(arguments[0]);
break;
// persist a given state at a given timestamp or persist a TimeSeries in a given service
case 2:
PersistenceExtensions.persist(this.rawItem, timestamp, _toOpenhabPrimitiveType(state));
if (_isTimeSeries(arguments[0])) {
this.#persistTimeSeries(arguments[0], arguments[1]);
break;
}
this.#persistGivenState(arguments[0], arguments[1]);
break;
// persist a given state at a given timestamp in a given service
case 3:
PersistenceExtensions.persist(this.rawItem, timestamp, _toOpenhabPrimitiveType(state), serviceId);
this.#persistGivenState(arguments[0], arguments[1], arguments[2]);
break;
// persist the current state or a TimeSeries
// default case
default:
PersistenceExtensions.persist(this.rawItem, ...arguments);
break;
}
}

// TODO: Add persist for TimeSeries
/**
* Internal method to persist the current state to a optionally given persistence service.
* @param {string} [serviceId]
*/
#persistCurrentState (serviceId) {
log.debug(`Persisting current state of Item ${this.rawItem.getName()}${serviceId ? ' to ' + serviceId : ''} ...`);
if (serviceId) {
PersistenceExtensions.persist(this.rawItem, serviceId);
return;
}
PersistenceExtensions.persist(this.rawItem);
}

/**
* Internal method to persist a given state at a given timestamp to a optionally given persistence service.
* @param {(time.ZonedDateTime | Date)} timestamp
* @param {string|number|time.ZonedDateTime|Quantity|HostState} state
* @param {string} [serviceId]
*/
#persistGivenState (timestamp, state, serviceId) {
if (typeof timestamp !== 'object') throw new TypeError('persist(timestamp, state, serviceId): timestamp must be a ZonedDateTime or Date object!');
log.debug(`Persisting given state ${state} of Item ${this.rawItem.getName()}${serviceId ? ' to ' + serviceId : ''} ...`);
if (serviceId) {
PersistenceExtensions.persist(this.rawItem, timestamp, _toOpenhabPrimitiveType(state), serviceId);
return;
}
PersistenceExtensions.persist(this.rawItem, timestamp, _toOpenhabPrimitiveType(state));
}

/**
* Internal method to persist a given TimeSeries to a optionally given persistence service.
* @param {items.TimeSeries} timeSeries
* @param {string} [serviceId]
*/
#persistTimeSeries (timeSeries, serviceId) {
log.debug(`Persisting TimeSeries for Item ${this.rawItem.getName()}${serviceId ? ' to ' + serviceId : ''} ...`);
// Get accepted data types of the Item to use the TypeParser
const acceptedDataTypes = this.rawItem.getAcceptedDataTypes();
// Create a Java TimeSeries object from the JS TimeSeries
const ts = new TimeSeries(TimeSeries.Policy.valueOf(timeSeries.policy));
timeSeries.states.forEach(([timestamp, state]) => {
ts.add(timestamp, TypeParser.parseState(acceptedDataTypes, _toOpenhabPrimitiveType(state)));
});
// Persist the Java TimeSeries
if (serviceId) {
PersistenceExtensions.persist(this.rawItem, ts, serviceId);
return;
}
PersistenceExtensions.persist(this.rawItem, ts);
}

/**
* Retrieves the persisted state for a given Item at a certain point in time.
Expand Down
29 changes: 15 additions & 14 deletions src/items/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,11 @@ const { UnDefType, OnOffType, events, itemRegistry } = require('@runtime');
const metadata = require('./metadata/metadata');
const ItemPersistence = require('./item-persistence');
const ItemSemantics = require('./item-semantics');
const TimeSeries = require('./time-series');

const itemBuilderFactory = osgi.getService('org.openhab.core.items.ItemBuilderFactory');

// typedefs need to be global for TypeScript to fully work
/**
* @typedef {import('../items/metadata/metadata').ItemMetadata} ItemMetadata
* @private
*/
/**
* @typedef {import('@js-joda/core').ZonedDateTime} time.ZonedDateTime
* @private
*/
/**
* @typedef {import('../quantity').Quantity} Quantity
* @private
*/

/**
* @typedef {object} ItemConfig configuration describing an Item
* @property {string} type the type of the Item
Expand All @@ -45,6 +33,18 @@ const itemBuilderFactory = osgi.getService('org.openhab.core.items.ItemBuilderFa
* @property {string} [giBaseType] the group Item base type for the Item
* @property {HostGroupFunction} [groupFunction] the group function used by the Item
*/
/**
* @typedef {import('../items/metadata/metadata').ItemMetadata} ItemMetadata
* @private
*/
/**
* @typedef {import('@js-joda/core').ZonedDateTime} time.ZonedDateTime
* @private
*/
/**
* @typedef {import('../quantity').Quantity} Quantity
* @private
*/

/**
* Tag value to be attached to all dynamically created Items.
Expand Down Expand Up @@ -616,7 +616,8 @@ const itemProperties = {
replaceItem,
removeItem,
Item,
metadata
metadata,
TimeSeries
};

/**
Expand Down
Loading

0 comments on commit 2791284

Please sign in to comment.