Skip to content

Skripte

mdzio edited this page Jun 9, 2024 · 28 revisions

Skripte

Der CCU-Historian besitzt eine Skriptumgebung, die direkten Zugriff auf die Datenbank besitzt. Dadurch eröffnen sich vielfältige Anwendungen: Automatisierte Massenkonfiguration, Erstellung von Statistiken und Analysen, Manipulation von Zeitreihen, usw.. Skripte können über WerkzeugeSkriptumgebung eingegeben und ausgeführt werden. Weitere Informationen sind im Abschnitt Skriptumgebung zu finden. In den folgenden Abschnitten sind Beispielskripte aufgeführt. In der Regel besitzen die Skripte am Anfang einen Konfigurationsabschnitt, der vor Ausführung des Skripts sorgfältig bearbeitet werden sollte.

Zeitreihenberechnungen (ab V3.4.0)

Skripte können auch nur aus einem Berechnungsausdruck (Formel, Term) mit Zeitreihen bestehen (siehe auch Abschnitt im Handbuch). Beispiele zu Berechnungsausdrücken sind in einem eigenen Abschnitt zu finden.

Automatische Ausführung von Skripten

Skripte können auch zyklisch automatisch ausgeführt werden (ab V3.2.0). Die unten aufgeführten Skripte können zu diesem Zweck auch in der Konfigurationsdatei ccu-historian.config eingefügt werden.

Skript-Beispiele

Massenkonfiguration der Delta- und Swinging-Door-Kompression

Mit dem folgenden Skript wird die Delta- und Swinging-Door-Kompression automatisch für alle Datenpunkt gesetzt. Gerade die Swinging-Door-Kompression sollte aber noch manuell nachjustiert werden, da bei vielen Datenpunkten eine Abweichung viel größer als 0,01 erlaubt sein sollte.

/*
Autokonfiguration Deltakompression V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

// *** Konfiguration ***

// Testlauf durchführen? Bei einem Testlauf wird die Datenbank nicht verändert.
// (Ja: true, Nein: false)
def testRun=true

// Sollen bereits konfigurierte Vorverarbeitungen überschrieben werden? 
// (Ja: true, Nein: false)
def overrideAll=true

// Sollen auch nicht geänderte Datenpunkte aufgelistet werden?
// (Ja: true, Nein: false)
def logNotChanged=false

// *** Skript ***
println "*** Autokonfiguration Deltakompression V1.0.0 ***"
println "Testlauf: ${testRun?"Ja":"Nein"}"
println "Alle überschreiben: ${overrideAll?"Ja":"Nein"}"
println "Alle auflisten: ${logNotChanged?"Ja":"Nein"}"

database.dataPoints.each { dp ->
  
  def currentCompr=getPreprocType(dp)
  def currentParam=getPreprocParam(dp)
  if (currentCompr!=PreprocType.DISABLED && !overrideAll) {
    println "\n$dp.displayName"
    println "  Vorhandene Vorverarbeitung wird nicht überschrieben: $currentCompr, $currentParam"
    return
  }
  
  def type=dp.attributes.type
  def ident=dp.id.identifier
  def compr=currentCompr
  def param=currentParam
  
  if (type=="ACTION") {
    compr=PreprocType.DISABLED
    param=0.0
	
  } else if (type in ["BOOL", "INTEGER", "ENUM", "ALARM"]) {
    compr=PreprocType.DELTA_COMPR
    param=0.1  
	
  } else if (type=="FLOAT") {
    compr=PreprocType.SWD_COMPR
    param=0.01  
	
  } else if (type=="STRING") {
    compr=PreprocType.DELTA_COMPR
    param=0.0
  }
  
  if (currentCompr!=compr || currentParam!=param) {
    println "\n$dp.displayName"
    println "  Vorverarbeitung wird abgeändert: $compr, $param"
    if (!testRun) {
      dp.attributes.preprocType=compr.ordinal()
      dp.attributes.preprocParam=(compr==PreprocType.DISABLED ? null : param)
      database.updateDataPoint(dp)
    }
  } else {
    if (logNotChanged) {
      println "\n$dp.displayName"
      println "  Vorverarbeitung muss nicht geändert werden: $compr, $param"
    }
  }
}

def getPreprocType(dp) {
  int idx=(dp.attributes.preprocType as Integer)?:PreprocType.DISABLED.ordinal()
  if (idx<0 || idx>=PreprocType.values().length) {
    throw new Exception("Invalid preprocessing type in database: "+idx)
  }
  PreprocType.values()[idx]
}

def getPreprocParam(dp) {
  (dp.attributes.preprocParam as Double)?:0.0
}

Massenkonfiguration der zu versteckenen Datenpunkte

/*
Autokonfiguration Versteckt V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

// *** Konfiguration ***

// Testlauf durchführen? Bei einem Testlauf wird die Datenbank nicht verändert.
// (Ja: true, Nein: false)
def testRun=true

// Sollen bereits verstecke Datenpunkte sichtbar gemacht werden?
// (Ja: true, Nein: false)
def unhide=false

// Folgende Datenpunkte sollen versteckt werden:
def toHide=[
  "WORKING",
  "STICKY_UNREACH",
  "UNREACH",
  "ERROR",
  "INSTALL_TEST",
  "CONFIG_PENDING",
  "LOWBAT",
  "BOOT",
  "ERROR_SABOTAGE",
  "STICKY_SABOTAGE",
  "DIRECTION",
  "ERROR_CODE",
  "LOW_BAT",
  "OPERATING_VOLTAGE",
  "OPERATING_VOLTAGE_STATUS",
  "RSSI_DEVICE",
  "UPDATE_PENDING",
  "ACTIVITY_STATE",
  "LEVEL_STATUS",
  "PROCESS",
  "SECTION",
  "WEEK_PROGRAM_CHANNEL_LOCKS",
  "ACTUAL_TEMPERATURE_STATUS",
  "ERROR_OVERHEAT",
  "RSSI_PEER",
  "ERROR_NON_FLAT_POSITIONING",
  "HUMIDITY_STATUS",
  "ILLUMINATION_STATUS",
  "RAIN_COUNTER_OVERFLOW",
  "RAIN_COUNTER_STATUS",
  "SUNSHINEDURATION_OVERFLOW",
  "SUNSHINE_THRESHOLD_OVERRUN",
  "WIND_SPEED_STATUS",
  "WIND_THRESHOLD_OVERRUN",
  "ERROR_WIND_COMMUNICATION",
  "ERROR_WIND_NORTH",
  "TEMPERATURE_OUT_OF_RANGE",
  "DUTY_CYCLE",  
] as Set

// *** Skript ***
println "*** Autokonfiguration Versteckt V1.0.0 ***"
println "Testlauf: ${testRun?"Ja":"Nein"}"
println "Versteckte bei Bedarf wieder sichtbar machen: ${unhide?"Ja":"Nein"}"

database.dataPoints.each { dp ->
  def currentlyHidden=dp.historyHidden
  if (currentlyHidden && !unhide) {
    return
  }
  def wantedHidden=(dp.id.identifier in toHide)
  if (currentlyHidden!=wantedHidden) {
    println "\n$dp.displayName"
    println "  Änderung auf: ${wantedHidden?"Versteckt":"Sichtbar"}"
    if (!testRun) {
      dp.historyHidden=wantedHidden
      database.updateDataPoint(dp)
    }
  }
}

Inaktive Datenpunkte verstecken

/*
Inaktive Datenpunkte verstecken V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

// *** Konfiguration ***

// Testlauf durchführen? Bei einem Testlauf wird die Datenbank nicht verändert.
// (Ja: true, Nein: false)
def testRun=true

// Sollen bereits verstecke Datenpunkte sichtbar gemacht werden?
// (Ja: true, Nein: false)
def unhide=false


// *** Skript ***
println "*** Inaktive Datenpunkte verstecken V1.0.0 ***"
println "Testlauf: ${testRun?"Ja":"Nein"}"
println "Versteckte bei Bedarf wieder sichtbar machen: ${unhide?"Ja":"Nein"}"

database.dataPoints.each { dp ->
  def currentlyHidden=dp.historyHidden
  if (currentlyHidden && !unhide) {
    return
  }
  
  // Verstecke den Datenpunkt, wenn er nicht aufgezeichnet wird
  def wantedHidden=dp.historyDisabled
  
  if (currentlyHidden!=wantedHidden) {
    println "\n$dp.displayName"
    println "  Änderung auf: ${wantedHidden?"Versteckt":"Sichtbar"}"
    if (!testRun) {
      dp.historyHidden=wantedHidden
      database.updateDataPoint(dp)
    }
  }
}

Versteckte Datenpunkte deaktivieren

/*
Versteckte Datenpunkte deaktivieren V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

// *** Konfiguration ***

// Testlauf durchführen? Bei einem Testlauf wird die Datenbank nicht verändert.
// (Ja: true, Nein: false)
def testRun=true

// Sollen bereits deaktiverte Datenpunkte wieder aktiviert werden, wenn sie nicht versteckt sind?
// (Ja: true, Nein: false)
def activate=false


// *** Skript ***
println "*** Versteckte Datenpunkte deaktivieren V1.0.0 ***"
println "Testlauf: ${testRun?"Ja":"Nein"}"
println "Inaktive bei Bedarf wieder aktiv schalten: ${activate?"Ja":"Nein"}"

database.dataPoints.each { dp ->
  def currentlyDisabled=dp.historyDisabled
  if (currentlyDisabled && !activate) {
    return
  }
  
  // Deaktiviere den Datenpunkt, wenn er versteckt ist
  def wantedDisabled=dp.historyHidden
  
  if (currentlyDisabled!=wantedDisabled) {
    println "\n$dp.displayName"
    println "  Änderung auf: ${wantedDisabled?"Inaktiv":"Aktiv"}"
    if (!testRun) {
      dp.historyDisabled=wantedDisabled
      database.updateDataPoint(dp)
    }
  }
}

Alle Historien leeren

Achtung: Durch dieses Skript werden Daten gelöscht!

/*
Alle Historien leeren V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

println "Historien werden geleert:"
def totalCnt=0
database.dataPoints.each { dp ->
    def cnt=database.deleteTimeSeries(dp, null, null)
    totalCnt+=cnt
    println "$dp.displayName: $cnt Einträge"
}
println "Gesamtanzahl der gelöschten Einträge: $totalCnt"

Damit auch die Datenbankdatei verkleinert wird, muss die Datenbank kompaktiert werden. Dies erfolgt mit der Kommandozeilenoption -compact (s.a. Handbuch Abschnitt Startparameter).

Alte Zeitreihendaten löschen

Achtung: Durch dieses Skript werden Daten gelöscht!

/*
Alte Zeitreihendaten löschen V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

// *** Konfiguration ***

// Anzahl der Tage, die in der Datenbank verbleiben sollen.
def daysToKeep = 365

// Testlauf durchführen? Bei einem Testlauf wird die Datenbank nicht verändert.
// (Ja: true, Nein: false)
def testRun = true

// *** Skript ***

println "Alte Zeitreihendaten werden gelöscht:"
def deleteDate=new Date()-daysToKeep
println "Löschdatum: ${deleteDate.format("dd.MM.YYYY")}"
def totalCnt=0
database.dataPoints.each { dp ->
    def cnt
    if (testRun) {
        cnt=database.getCount(dp, null, deleteDate)
    } else {
        cnt=database.deleteTimeSeries(dp, null, deleteDate)
    }
    totalCnt+=cnt
    println "$dp.displayName: $cnt Einträge"
}
println "Gesamtanzahl der gelöschten Einträge: $totalCnt"

Damit auch die Datenbankdatei verkleinert wird, muss die Datenbank kompaktiert werden. Dies erfolgt mit der Kommandozeilenoption -compact (s.a. Handbuch Abschnitt Startparameter).

Histrorien von inaktiven Datenpunkten löschen

Falls Datenpunkte nachträglich auf Inaktiv in der Datenpunktkonfiguration gesetzt werden, haben sich unter Umständen schon etliche Einträge in den zugehörigen Tabellen angesammelt. Diese können mit dem folgenden Skript gelöscht werden:

/*
Historien inaktiver Datenpuntke leeren V1.0.0
Wichtig: Vor Anwendung des Skripts ein Backup der Datenbank erstellen!
*/

// *** Konfiguration ***

// Testlauf durchführen? Bei einem Testlauf wird die Datenbank nicht verändert.
// (Ja: true, Nein: false)
def testRun = true

// *** Skript ***

def totalCnt=0
println "Historien inaktiver Datenpunkte werden gelöscht:"
println "Testlauf: ${testRun?"Ja":"Nein"}"

database.dataPoints.each { dp ->
  if (!dp.historyDisabled) {
    return
  }
  def cnt
  if (testRun) {
     cnt = database.getCount(dp, null, null)
  } else {
     cnt = database.deleteTimeSeries(dp, null, null)
  }
  totalCnt += cnt
  println "$dp.displayName: $cnt Einträge gelöscht"
}
println "Gesamtanzahl der gelöschten Einträge: $totalCnt"

Damit auch die Datenbankdatei verkleinert wird, muss die Datenbank kompaktiert werden. Dies erfolgt mit der Kommandozeilenoption -compact (s.a. Handbuch Abschnitt Startparameter).

Statistiken von einem Datenpunkt über einen Zeitbereich berechnen

Mit diesem Skript wird der korrekte Mittelwert berechnet, auch wenn die Delta-Kompression für einen Datenpunkt aktiviert wurde. Durch die Delta-Kompression ist der Abstand zwischen den Messwerten nicht mehr konstant!

// Datenpunkt mit der ID 736 holen (s.a. Datenpunktliste letzte Spalte oder Datenpunktdetails)
def dp=database.getDataPoint(736)
println "Datenpunkt: $dp.displayName"

def end=new Date() // aktueller Zeitpunkt
def duration=24*60*60*1000 // ein Tag zurück in Millisekunden
def begin=new Date(end.time-duration) 
println "Beginn: $begin, Ende: $end"

// Zeitreihe holen
def ts=database.getTimeSeries(dp, begin, end)

// Statistik berechnen
def min=Double.POSITIVE_INFINITY
def max=Double.NEGATIVE_INFINITY
def integr=0
def previous
ts.each { pv ->
  if (pv.value<min) min=pv.value
  if (pv.value>max) max=pv.value
  if (previous!=null) {
    // Teilintegral berechnen: Messwert*Millisekunden
    integr+=previous.value*(pv.timestamp.time-previous.timestamp.time)
  }
  previous=pv
}
// Durchnitt ist Integral/Zeitbereichslänge in Millisekunden.
def avg=integr/duration

println "Einträge: $ts.size, Einträge pro Stunde: ${ts.size/duration*60*60*1000}"
println "Minimum: $min, Maximum: $max, Integral: $integr, Durchschnitt: $avg"

Beispielausgabe:

Datenpunkt: Wetterdaten.ACTUAL_TEMPERATURE
Beginn: Wed May 20 17:56:11 CEST 2020, Ende: Thu May 21 17:56:11 CEST 2020
Einträge: 380, Einträge pro Stunde: 15.8331600000
Minimum: 9.8, Maximum: 29.9, Integral: 1.6324556046000009E9, Durchschnitt: 18.89416209027779

Rangliste der Anzahl der Einträge in einem Zeitbereich

Für alle Datenpunkte wird die Anzahl der Einträge in der Datenbank für den angegebenen Zeitbereich ermittelt. Die Datenpunkte werden dann beginnend mit der höchsten Anzahl an Einträgen aufgelistet.

// Rangliste der Anzahl der Einträge in einem Zeitbereich, V1.0

// *** Konfiguration ***

// Beginn des Zeitbereichs
def begin=Date.parse('yyyy-MM-dd hh:mm', '2021-03-29 00:00')
// Ende des Zeitbereichs
def end=Date.parse('yyyy-MM-dd hh:mm', '2021-03-29 22:00')

// *** Skript ***

def dpCount =[]
database.dataPoints.each { dp ->
    def cnt=database.getCount(dp, begin, end)
    dpCount << [cnt, dp.displayName]
}
println " ANZAHL DATENPUNKT"
dpCount.sort { -it[0] }.take(100).each {
    println it[0].toString().padLeft(7) + " " + it[1]
}

Backup der Datenbank anstoßen

Mit dem folgenden Skript kann sofort ein Backup angestoßen werden.

def timestamp=new Date().format("YYYYMMddHHmmss")
def fileName="db-backup_${timestamp}.zip"
database.createBackup(fileName)
println "Backup wurde in Datei $fileName geschrieben."

Benutzerdatenpunkt anlegen

Dieses Skript kann erst ab Version 3.3.0 ausgeführt werden. Es kann wiederholt ausgeführt werden. Falls der Datenpunkt bereits existiert, werden nur die Eigenschaften aktualisiert.

// *** Benutzerdatenpunkt anlegen V1.0.0 ***

// Für Benutzerdatenpunkte muss immer die Schnittstelle "User" verwendet werden.
createDataPoint("User", "000001:1", "COUNTER") {

  // Eigenschaften setzen.
  attributes.displayName="Stromzähler"
  attributes.room="Vorratsraum"
  attributes.function="Energie/Wasser"
  attributes.comment="Manuell abgelesener Stromzähler"

  // Messbereich und Einheit setzen.
  attributes.maximum=999999999
  attributes.unit="kWh"
  attributes.minimum=0

  // Folgende Typen sind zulässig: FLOAT, STRING, BOOL, ACTION, ALARM, INTEGER, ENUM
  attributes.type="FLOAT"

  // Bei stetigen Messwerten auf true setzen (z.B. Zähler, Temperatur, Füllstand) und bei
  // unstetigen auf false setzen (z.B. Schalter, Aufzählung). 
  continuous=true
}

// *** Skript ***

def createDataPoint(interfaceId, address, identifier, configure) {
  def id=new DataPointIdentifier(interfaceId, address, identifier)
  def dp=database.getDataPoint(id)
  def exists=(dp!=null)
  if (!exists) {
    dp=new DataPoint(id: id)
  }
  configure.delegate=dp
  configure.resolveStrategy=Closure.DELEGATE_ONLY
  configure()
  dp.historyString=(dp.attributes.type=="STRING")
  if (exists) {
    database.updateDataPoint(dp)
    println "Datenpunkt $id wurde aktualisiert."
  } else {
    database.createDataPoint(dp)
    println "Datenpunkt $id wurde neu angelegt."
  }
}

Zeitreihe per Skript einfügen

Dieses Skript kann erst ab Version 3.3.0 ausgeführt werden.

// *** Zeitreihenimport über Skript V1.0.0 ***

// Datenpunkt für den Import (Schnittstelle, Adresse, Parameter)
def id=new DataPointIdentifier("User", "000001:1", "COUNTER")

// Importierten Zeitbereich vorher leeren? (true=Ja; false=Nein)
def purge=true

// Zeitreihe als Tab-separierte Werte (z.B. Kopieren/Einfügen aus Excel)
// Die dritte Spalte enthält den Zustand des jeweiligen Messwertes (3=gut).
def tsv='''\
01.08.2022 00:00:00	1,00	3
02.08.2022 00:00:00	6,00	3
03.08.2022 00:00:00	4,00	3
04.08.2022 00:00:00	9,50	3
05.08.2022 00:00:00	6,00	3
06.08.2022 00:00:00	3,00	3
07.08.2022 00:00:00	2,00	3
08.08.2022 00:00:00	1,00	3
09.08.2022 00:00:00	6,00	3
10.08.2022 00:00:00	8,12	3
11.08.2022 00:00:00	7,00	3
12.08.2022 00:00:00	9,00	3
13.08.2022 00:00:00	4,00	3
14.08.2022 12:34:56	3,00	3
'''

// *** Skript ***

def dataPoint=database.getDataPoint(id)
if (dataPoint==null) {
  println "Datenpunkt $id nicht gefunden."
}
def timeSeries=new TimeSeries(dataPoint)
tsv.eachLine { line ->
  def fields=line.split("\t")
  if (fields.size()!=3) {
    throw new Exception("Ungültige Anzahl an Spalten in Zeile: $line")
  }
  Date timestamp=parseDate(fields[0])
  if (timestamp==null) {
    throw new Exception("Ungültiger Zeitstempel in Zeile: $line")
  }
  double value=parseNumber(fields[1])
  if (value==null) {
    throw new Exception("Ungültiger Wert in Zeile: $line")
  }
  int state=parseNumber(fields[2])
  if (state==null) {
    throw new Exception("Ungültige Zustand Zeile: $line")
  }
  timeSeries.add(new ProcessValue(timestamp, value, state))
}
if (timeSeries.size>0) {
  def firstTimestamp=timeSeries[0].timestamp  
  def lastTimestamp=timeSeries[timeSeries.size-1].timestamp
  println "$timeSeries.size Einträge werden im Zeitbereich von ${format(firstTimestamp)} " +
    "bis ${format(lastTimestamp)} beim Datenpunkt $id eingefügt."
  if (purge) {
    database.deleteTimeSeries(dataPoint, firstTimestamp, new Date(lastTimestamp.time+1))
    println "Zeitbereich wurde geleert."
  }
  database.insertTimeSeries(timeSeries)
  println "Einträge wurden eingefügt."
} else {
  println "Keine Einträge zum Einfügen angegeben."
}

Zählerablesungen einfügen

Dieses Skript kann erst ab Version 3.3.0 ausgeführt werden.

// *** Manuelle Ablesungen einfügen V1.0.1 ***

def timestamp="09.05.2024 10:15"
def readings=[
  2940,
  1404.5582,
  953.656,
  259,
]
def dataPoints=[
  "User.000001:1.COUNTER", // Strom
  "User.000001:2.COUNTER", // Wasser
  "User.000001:3.COUNTER", // Gas
  "User.000001:4.COUNTER", // Strom Einspeisung
]

// *** Skript ***

def ts=parseDate(timestamp)
readings.eachWithIndex { reading, idx -> 
  def dp=database.getDataPoint(new DataPointIdentifier(dataPoints[idx]))
  def series=new TimeSeries(dp)
  series.add(new ProcessValue(ts, reading, 3))
  database.insertTimeSeries(series)
}
println "Fertig"

Meta-Informationen aus der CCU zu einem Datenpunkt korrigieren

Die CCU meldet für einige wenige Datenpunkte keine oder falsche Meta-Informationen (z.B. Min, Max, Einheit). Das kann bei der Trend-Darstellung unschön sein, da der CCU-Historian diese Daten für die Darstellung verwendet. Datenpunkte mit der gleichen Einheit werden z.B. auf dieselbe Skala gesetzt. Mit folgenden Skript können die Meta-Daten korrigiert werden.

// *** Meta-Informationen zu einem Datenpunkt korrigieren V1.0.0 ***

updateDataPoint("BidCos-RF", "GEQ0xxxxxx:1", "BRIGHTNESS") {

  // Eigenschaften setzen.
  attributes.displayName="Flur KG Helligkeit"
  attributes.room="Flur KG"
  attributes.function="Licht"

  // Messbereich und Einheit setzen.
  attributes.maximum=100000
  attributes.unit="Lux"
  attributes.minimum=0

  // Bei stetigen Messwerten auf true setzen (z.B. Zähler, Temperatur, Füllstand) und bei
  // unstetigen auf false setzen (z.B. Schalter, Aufzählung). 
  continuous=true

  // Zyklisches Aktualisieren der Meta-Informationen aus der CCU abschalten.
  // Ansonsten werden die obigen Einstellungen irgendwann wieder überschrieben.
  noSynchronization=true
}

// *** Skript ***

def updateDataPoint(interfaceId, address, identifier, configure) {
  def id=new DataPointIdentifier(interfaceId, address, identifier)
  def dp=database.getDataPoint(id)
  if (dp==null) {
    throw new Exception("Datenpunkt $id nicht gefunden.")
  }
  configure.delegate=dp
  configure.resolveStrategy=Closure.DELEGATE_ONLY
  configure()
  database.updateDataPoint(dp)
  println "Datenpunkt $id wurde aktualisiert."
}

Wöchentliches Komprimieren und Löschen von alten Zeitreihen

Da das Skript zyklisch jede Woche ausgeführt werden soll, muss es zur Konfigurationsdatei ccu-historian.config hinzugefügt werden (siehe auch Automatische Ausführung von Skripten).

Dieses Skript wurde von wak aus dem HomeMatic-Forum zur Verfügung gestellt. Eine Diskussion zu diesem Skript ist hier zu finden.

database.tasks.DelCompr.enable=true
database.tasks.DelCompr.cron="0 0 3 ? * SUN"
database.tasks.DelCompr.script={

	// Datenreihen löschen und komprimieren, V1.0
	  
	// Mit der Variable testRun=false kann ihn den Schreib-Modus gewechselt werden und 
	// damit werden Daten gelöscht und / oder //komprimiert.
	// (Ja: true, Nein: false)
	def testRun = false

	// Datensätze für die nächste Komprimierung zu wiederholen, 
	// dazu muß Varialbe rekompress = true gesetz werden. Nach erfolgreichen Lauf sollte das wieder 
	// zurückgesetzt werden auf rekompress = false, damit das Script viel schneller läuft!
	// (Ja: true, Nein: false)
	// Bei einer Anpassung der config kann es erforderlich sein existierende komprimierte 
	def rekompress = false

	/*
	Mit der Variable config kann man die gewünschten Datenpunkte konfigurieren:
	Parameter 1 z.b. "*DutyCycle*" ist der Displayname und es können * (Wildcards verwendet werden)
	Parameter 2 z.b. 365 ist die Anzahl der Tage die in der Datenbank verbleiben sollen, 365 wäre hier ein Jahr,
					 alle Daten drüber ob komprimiert oder nicht werden gelöscht (es gibt auch volle Monate
					 oder volle Jahre "5M" oder "3Y" )
	Parameter 3 z.b. 10 ist hier die Anzal der Tage nachdem die Werte komprimiert werden
	Parameter 4 z.b. 60*60*1 bestimmt den Komprimierungszeitraum das wäre 1 Std. (gängige Werte für mich 60*60*2 oder 60*60*24)
	Parameter 5 z.b. "max" bestimmt die Art der Komprimierung "max" = maximaler Wert in 1 Std. ebenso gibt es noch:
	 			     "min" - Minimuwert in 1Std.
					 "avg" - Durchschnittswert verwende ich bei Temperaturwerten oder Feuchtigkeitswerten
					 "first" - erster Wert im Komprimierungszeitraum verwende ich bei Zählern
					 "last" - letzter Wert im Komprimierungszeitraum
					 "integral" - Durchschittswert mit Zeitfaktor, funktioniert aber nur bei vielen Wert
								  in einem Komprimierungszeitraum spricht Tagesrdurchschnitt
	 */
   
    // min, max, first, last or avg as value for compression
    def config = [
        ["*DUTYCYCLE*"            ,"3Y", 10, 60*60*4,   "max" ],    // 4 Std Max-Wert nach 10 Tagen und nach 3 Jahren löschen
        ["*CARRIER_SENSE*"        ,"3Y", 10, 60*60*4,   "max" ],    // 4 Std Max-Wert nach 10 Tagen und nach 3 Jahren löschen 
        ["*HUMIDITY"              ,1000, 30, 60*60*2,   "avg" ],    // 2 Std Durchsch. nach 30 Tagen und nach 1000 Tagen löschen
        ["*TEMPERATURE"           ,1000, 30, 60*60*2,   "avg" ],    // 2 Std Durchsch. nach 30 Tagen und nach 1000 Tagen löschen
        ["*4.VALVE_STATE"         ,1000, 30, 60*60*3,   "avg" ],    // 3 Std Durchsch. nach 30 Tagen und nach 1000 Tagen löschen
        ["*.ENERGY_COUNTER"       ,1000, 30, 60*60*2,   "first" ],  // 2 Stunden bei Zähler immer den 1. Wert nehmen nach 30 Tagen und nach 1000 Tagen löschen
        ["*.POWER"                ,1000, 30, 60*60*2,   "max" ],    // 2 Std max nach 30 Tagen und nach 1000 Tagen löschen
        ["*.POWER_STATUS"         , 730, 30, 60*60*2,   "avg" ],    // 2 Std Durchsch. nach 30 Tagen und nach 730 Tagen löschen
        ["*.FREQUENCY_STATUS"     , 730, 10, 60*60*3,   "avg" ],  // 3 Std Durchsch. nach 10 Tagen und nach 730 Tagen löschen
        ["*.CURRENT_STATUS"       , 730, 10, 60*60*3,   "avg" ],    // 3 Std Durchsch. nach 10 Tagen und nach 730 Tagen löschen
        ["*.VOLTAGE_STATUS"       , 730, 10, 60*60*3,   "avg" ],    // 3 Std Durchsch. nach 10 Tagen und nach 730 Tagen löschen
        ["*.ENERGY_COUNTER_OVERFLOW" ,730, 10, 60*60*3, "avg" ],   // 3 Std Durchsch. nach 10 Tagen und nach 730 Tagen löschen
    ] 

	// *** Skript *************************

	def dateFormat = "yyyy-MM-dd"   // standard Timestamp Format
	def jetzt =  new Date()               // aktuellen StartTimeStamp speichern für Berechnungen

	// 24 Bits für Flag berechnen
	def int bit24 = Math.pow(2,23)   // 8388608;

	config.each { conf ->

		// Löschdatum 5Y oder 3M auf Tage aktuell umrechnen mit 1. des Monats
		def orgConf = conf[1]
		if (conf[1].getClass() == String ) {
			if (conf[1].endsWith("Y") && conf[1].length()>=2) {
				if ( conf[1][0..-2].isInteger() && conf[1][0..-2].toInteger() > 0 ) {
					def yearValue = conf[1][0..-2].toInteger()
					def dateTmp = new Date(year: (jetzt.getYear() - yearValue ) , month: 0, date: 1, hours: 0, minutes: 0, seconds: 0)
					conf[1] = jetzt - dateTmp     // berechnete Löschtage setzen für xY
				} else {
					conf[1] = 0   // bei falschen Werten auf 0 setzen und nix tun
				}
			} else if (conf[1].endsWith("M") && conf[1].length()>=2) {
				if ( conf[1][0..-2].isInteger() && conf[1][0..-2].toInteger() > 0 ) {
					def monthValue = conf[1][0..-2].toInteger()
					def cal=Calendar.getInstance().clearTime()
					cal.add(Calendar.MONTH, monthValue * -1);
					cal.set(Calendar.DAY_OF_MONTH, 1);
					conf[1] = jetzt - cal.getTime() // berechnete Löschtage setzen für xM
				} else {
					conf[1] = 0   // bei falschen Werten auf 0 setzen und nix tun
				}
			} else {
				conf[1] = 0   // bei falschen Werten auf 0 setzen und nix tun
			}
			def delDate = jetzt - conf[1]
			delDate.clearTime()
			println conf[0].padRight(40) + " Löschdatum berechnet " + delDate.format(dateFormat) + "  " + orgConf + "  " + conf[1]
		}

		// wildcards in regex umbauen
		def txtResult = new StringBuffer()
		conf[0].each { ch ->
			switch (ch) {
				case '*':
				// Single '*' matches single dir/file; Double '*' matches sequence of zero or more dirs/files
					txtResult << /[^\/]*/
					break
				case '?':
				// Any character except the normalized file separator ('/')
					txtResult << /[^\/]/
					break
				case [
					'$',
					'|',
					'[',
					']',
					'(',
					')',
					'.',
					':',
					'{',
					'}',
					'\\',
					'^',
					'+'
				]:
					txtResult << '\\' + ch
					break
				default: txtResult << ch
			}
		}
		conf[0] = txtResult.toString().toUpperCase()
		// println conf[0]
	}

	def comprFactor=0
	def dpCount=[]
	def summeDel=0
	def summeComp=0
	def summetotal=0
	def lastDPState
	database.dataPoints.sort{ it.displayName.toUpperCase() }.each { dp ->

		// check dp.displayName wird in config gefunden
		def found=null
		def delBegin=null
		def delEnd=null
		def komprBegin=null
		def cntNew

		// Datenpunkt in Liste über REGEX suchen
		config.find { con ->
			if (dp.displayName.toUpperCase()==~con[0]) {
				// println  con[0] + " -> " + dp.displayName    // check REGEX Regeln
				found = con
				// beim ersten gefunden, merken und Schleife verlassen
				return true // break
			}
		}
		if (found) {

			// Datenlöschung     *******
			if (found[1] > 10) {
				cntNew=0
				// Löschzeitraum bestimmen
				delBegin=database.getFirstTimestamp(dp)
				delEnd=jetzt-found[1]      // found[1] = 2 spalte aus der Konfiguration Tabelle am Anfang
				delEnd.clearTime()         // Zeit auf 00:00:00 stellen

				if (testRun) {
					cntNew=database.getCount(dp, delBegin  , delEnd)
					if (cntNew>0) println "$dp.displayName: $cntNew werden gelöscht! von: " + delBegin.format(dateFormat) + "  bis: " + delEnd.format(dateFormat) + " (Testlauf)"
				} else {
					cntNew=database.deleteTimeSeries(dp, delBegin  , delEnd)
					if (cntNew>0) println "$dp.displayName: $cntNew gelöscht! von: " + delBegin.format(dateFormat) + "  bis: " + delEnd.format(dateFormat)
				}
				summeDel=summeDel+cntNew
			}

			// Datenkomprimierung   ******
			if (found[2] > 5) {
				// Komprimierungs Zeitraum bestimmen
				if (delEnd) {
					komprBegin=delEnd
				} else {
					komprBegin=database.getFirstTimestamp(dp)
				}
				def komprEnd=jetzt-found[2]
				komprEnd.clearTime()    // Zeit auf 00:00:00 stellen

				comprFactor = 1000*found[3]  // Sekunden von Konfigurationtabelle * 1000 auf Millisekungen
				def comprValue = found[4]    // von Konfigurationtalle "AVG", "MIN", "MAX", ...

				def cnt

				// Zeitreihe holen
				def ts=database.getTimeSeriesRaw(dp, komprBegin, komprEnd)

				def komprBeg = komprBegin
				if (!rekompress) {

					// erste Zeitreihe ohne komprimierung finden
					komprBeg = komprEnd
					ts.find { pv ->
						if (!(pv.state&bit24)) {
							komprBeg = pv.timestamp    // neues begin ermittelt
							return true // break
						}
					}
				}
				//println "Erste Timestamp ohne KompFlag: $komprBeg"

				// Zeitreihe holen neu ohne bereits komprimierte
				ts=database.getTimeSeriesRaw(dp, komprBeg , komprEnd)
				cnt=ts.size

				// Statistik berechnen
				def duration=komprEnd.time - komprBeg.time
				def min=Double.POSITIVE_INFINITY
				def max=Double.NEGATIVE_INFINITY
				def integr=0
				def intsum=0
				def intwert=0
				def anzahl=0
				def summe=0
				def lastTime=0
				def thisTime=0
				def firstValue=0
				def lastValue=0
				def avg=0
				def previous
				def comprPosible=false

				// neue komprimierte Zeitreihe erstellen
				def timeSeries=new TimeSeries(dp)

				ts.each { pv ->
					thisTime=new Date( ( ( Math.floor(pv.timestamp.time/comprFactor)*comprFactor) as long) )

					if (thisTime!=lastTime) {
						if (lastTime!=0) {
							duration=lastTime.time - thisTime.time
							// Durchnitt ist Integral/Zeitbereichslänge in Millisekunden.
							// def avg=integr/duration
							avg = Math.round(summe / anzahl * 10) / 10
							if (intsum!=0) intwert = Math.round(integr / intsum * 10) / 10

							// println lastTime.format(dateFormat) + " Anzahl: $anzahl, Minimum: $min, Maximum: $max, Integral: $intwert, Durchschnitt: $avg, Duration: $duration, First: $firstValue, Last: $lastValue"

							switch(comprValue) {
								case "min":
									timeSeries.add(new ProcessValue(lastTime, min, bit24|lastDPState ))
									break;
								case "max":
									timeSeries.add(new ProcessValue(lastTime, max, bit24|lastDPState ))
									break;
								case "first":
									timeSeries.add(new ProcessValue(lastTime, firstValue, bit24|lastDPState ))
									break;
								case "last":
									timeSeries.add(new ProcessValue(lastTime, lastValue, bit24|lastDPState ))
									break;
								case "avg":
									timeSeries.add(new ProcessValue(lastTime, avg, bit24|lastDPState ))
									break;
								case "integral":
									timeSeries.add(new ProcessValue(lastTime, avg, bit24|lastDPState ))
									break;
							}
							if (anzahl>1) comprPosible=true
						}

						min=pv.value
						max=pv.value
						anzahl=0
						summe=0
						integr=0
						intsum=0
						intwert=pv.value
						firstValue=pv.value
						lastDPState=pv.state
						lastTime=thisTime
					}

					if (pv.value<min) min=pv.value
					if (pv.value>max) max=pv.value
					if (previous!=null) {
						// Teilintegral berechnen: Messwert*Millisekunden
						integr+=previous.value*(pv.timestamp.time-previous.timestamp.time)
						intsum+=(pv.timestamp.time-previous.timestamp.time)
					}
					lastValue=pv.value
					anzahl=anzahl+1
					summe=summe+pv.value
					previous=pv
				}
				if (lastTime!=0) {
					duration=lastTime.time - thisTime.time
					// Durchnitt ist Integral/Zeitbereichslänge in Millisekunden.
					// def avg=integr/duration
					avg = Math.round(summe / anzahl * 10) / 10
					if (intsum!=0) intwert = Math.round(integr / intsum * 10) / 10

					// println lastTime.format(dateFormat) + " Anzahl: $anzahl, Minimum: $min, Maximum: $max, Integral: $intwert, Durchschnitt: $avg, Duration: $duration, First: $firstValue, Last: $lastValue"

					switch(comprValue) {
						case "min":
							timeSeries.add(new ProcessValue(lastTime, min, bit24|lastDPState ))
							break;
						case "max":
							timeSeries.add(new ProcessValue(lastTime, max, bit24|lastDPState ))
							break;
						case "first":
							timeSeries.add(new ProcessValue(lastTime, firstValue, bit24|lastDPState ))
							break;
						case "last":
							timeSeries.add(new ProcessValue(lastTime, lastValue, bit24|lastDPState ))
							break;
						case "avg":
							timeSeries.add(new ProcessValue(lastTime, avg, bit24|lastDPState ))
							break;
						case "integral":
							timeSeries.add(new ProcessValue(lastTime, avg, bit24|lastDPState ))
							break;
					}
					if (anzahl>1) comprPosible=true
				}
				if (comprPosible) {
					def comprFactorSek = comprFactor/1000
					println "$dp.displayName: Konfig " + komprBegin.format(dateFormat) + " bis: " + komprEnd.format(dateFormat) + " Zeitraum Sek: $comprFactorSek Wert: $comprValue"
					cntNew=0
					if (testRun) {
						println "$dp.displayName: komprimiert! $cnt -> $timeSeries.size von: " + komprBeg.format(dateFormat) + " bis: " + komprEnd.format(dateFormat) + " (Testlauf)"
						cntNew=timeSeries.size
					} else {
						println "$dp.displayName: komprimiert! $cnt -> $timeSeries.size von: " + komprBeg.format(dateFormat) + " bis: " + komprEnd.format(dateFormat)
						cntNew=database.replaceTimeSeries(dp, timeSeries, komprBeg, komprEnd)
					}
					summeComp=summeComp+ (cnt-cntNew)
					summetotal=summetotal+cnt
				}
			}
		}
	}

	println "Summe Datenzeilen gelöscht:    $summeDel"
	def delSum = summetotal - summeComp
	println "Summe Datenzeilen komprimiert: $summeComp von $summetotal  -> $delSum"

	use(groovy.time.TimeCategory) {
		def duration = new Date() - jetzt
		print "Laufzeit: Std.: ${duration.hours}, Min.: ${duration.minutes}, Sek.: ${duration.seconds}"
	}
}