SONOFF POW mit Tasmota – Trockner in OpenHAB einbinden

4. Februar 2019 at 22:48
Print Friendly, PDF & Email

Mit den POW Modulen von itead läßt sich der Energieverbrauch erfassen. Die Original-Firmware habe ich durch Tasmota ersetzt, da meine Geräte keine Daten an eine Cloud des Herstellers schicken soll.
Diese Firmware gibt es es für viele verschiedene Geräte, die alle auf dem ESP8266 basieren.

Man kann die Geräte über den Web-Server steuern und auslesen. Interessanter ist jedoch die Anbindung an OpenHAB mittels des MQTT Protokolls. Zuerst wollte ich MQTT vermeiden, mittlerweile ist dies mein erste Wahl, um Geräte im LAN miteinander kommunizieren zu lassen.

In OpenHAB habe ich Regeln erstellt, um Statusmeldungen per Telegramm zu schicken. Weiterhin kann Grafana die Daten sehr schöne Darstellung darstellen .

 Web-Oberfläche der Tasmota Firmware

Anzeige der Messwerte mit Grafana

Täglicher Verbrauch sowie kumulierter Verbrauch

Zeitlicher Verlauf pro Verbraucher

Leistung und Verbrauch pro Gerät. Verbrauch und Leistung können mit der rechten und linken Achse einzeln skaliert werden.

Heizung: Temperatur und Leistung

Die Temperatur der Heizung wird parallel mit 1-Wire Sensoren erfasst.
Hier sieht man in pink die Leistung und kann sehen, wie die Pumpen gesteuert werden.

Status vs. Verbrauch/Leistung

Der Status (Aus=0, Läuft=1, Fertig=3) der Geräte wird über den Verbrauch bestimmt. Die folgende Darstellung zeigt mir den Zusammenhang. Dies ist hilfreich, um die sinnvollen Punkte zu finden,

 

Die Umstellung auf die neue Version von MQTT in OpenHAB war nicht ganz einfach. Hierbei habe ich dann auch einen Fehler in OpenHAB gefunden, der bereits behoben wurde (Version 2.5.0).

Die Definition der Regeln war auch etwas aufwendiger, bis sie zufriedenstellend lief.

Bzgl. der Messungen war ich von den Stromschwankungen / Spikes überrascht. Evtl. ist dies eine Messungenauigkeit der POW Module (Version 1).

Folgend die Darstellung in der OpenHAB App.

Übersicht

 

bridge.things

Bridge mqtt:broker:MqttPandora20 "MQTT Broker Pandora" @ "MQTT Pandora" [ 
  host="192.168.1.20",
  port=1883,
  secure="AUTO",
  qos=0,
  retain=false,
  clientid="Oh2Mqtt2Thing20",
  keep_alive_time=30000,
  reconnect_time=60000,
  username="mqttusr20",
  password="mqttpass20"
]

sonoff.things

Thing mqtt:topic:KG_Trockner_thg "Sonoff POW Trockner1" (mqtt:broker:MqttPandora20) @ "MQTT Pandora"  {
  Channels:
    Type switch : PowerSwitch    "Power Switch 02" [ stateTopic="house/stat/trockner/POWER", commandTopic="house/cmnd/trockner/Power", on="1", off="0" ]
    Type switch : PowerSwitchRes "Switch State 02" [ stateTopic="house/stat/trockner/RESULT", transformationPattern="JSONPATH:$.POWER",on="ON",off="OFF"]
 
    Type string : Version        "Version 02"      [ stateTopic="house/tele/trockner/INFO1",  transformationPattern="JSONPATH:$.Version"]
    Type string : fallback       "fallback topic"  [ stateTopic="house/tele/trockner/INFO1",  transformationPattern="JSONPATH:$.FallbackTopic"]
    Type string : hostname       "hostname      "  [ stateTopic="house/tele/trockner/INFO2",  transformationPattern="JSONPATH:$.Hostname"]
    Type string : IP             "IP            "  [ stateTopic="house/tele/trockner/INFO2",  transformationPattern="JSONPATH:$.IPAddress"]
 
    Type string : time          "Time"             [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Time" ]
    Type string : uptime        "Uptime"           [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Uptime" ]
    Type number : vcc           "VCC"              [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Vcc"  ]
    Type string : wifi-ap       "Wifi AP"          [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Wifi.AP" ]
    Type string : wifi-ssid     "Wifi SSID"        [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Wifi.SSId" ]
    Type string : wifi-channel  "Wifi Channel"     [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Wifi.Channel" ]
    Type string : wifi-rssi     "Wifi RSSI"        [ stateTopic="house/tele/trockner/STATE", transformationPattern="JSONPATH:$.Wifi.RSSI" ]
 
    Type string : TimePOW       "POW Time"         [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.Time" ]
    Type number : Total         "POW Total"        [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Total" ]
    Type number : Voltage       "POW Voltage"      [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Voltage" ]
    Type number : Today         "POW Today"        [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Today" ]
    Type number : Power         "POW Power"        [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Power" ]
    Type number : PowerAVG      "POW Power AVG"    [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.ENERGY.Period" ]
//    Type number : AppPower      "POW App Power"    [ stateTopic="house/tele/trockner/SENSOR", transformationPattern="JSONPATH:$.ENERGY.ApparentPower" ]
    Type string : devicestate   "Device State"     [ stateTopic="house/tele/trockner/LWT" ]
    Type string : Pubstate      "POW publish"      [ commandTopic="house/stat/trockner/PUBLIC" ]
 
} // end of thing

sonoff.items

// KG_Trockner_Power
Number KG_Trockner_OpState      "Trockner Status [MAP(sonoffPOW.map):%s]" <washingmachine_2> (gSonoffSw2,gSonoffSw2Db)
Switch KG_Trockner_Switch 		  "SPs02 Switch 1" 	        (gSonoffSw2,gPersist)     { channel="mqtt:topic:KG_Trockner_thg:PowerSwitch" }
Switch KG_Trockner_State 		    "SPs02 State 1"           (gSonoffSw2,gPersist)     { channel="mqtt:topic:KG_Trockner_thg:PowerSwitchRes"}
Number KG_Trockner_Vcc 		      "SPs02 VCC  [%s]" 		    (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:vcc" }
String KG_Trockner_WifiAp 		  "SPs02 Wifi AP  [%s]" 	  (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:wifi-ap" }
String KG_Trockner_WifiSsid 	  "SPs02 Wifi SSID  [%s]" 	(gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:wifi-ssid" }
String KG_Trockner_WifiChannel  "SPs02 Wifi Channel [%s]" (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:wifi-channel" }
String KG_Trockner_WifiRssi 	  "SPs02 Wifi RSSI  [%s]" 	(gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:wifi-rssi" }
String KG_Trockner_Uptime 		  "SPs02 Uptime" 		        <time> (gSonoffSw2Info)   { channel="mqtt:topic:KG_Trockner_thg:uptime" }
String KG_Trockner_Time 		    "SPs02 Time" 		          <time> (gSonoffSw2Info)   { channel="mqtt:topic:KG_Trockner_thg:time" }
String KG_Trockner_Version 		  "SPs02 Version [%s]"      (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:Version" }
String KG_Trockner_Hostname 	  "SPs02 Hostname  [%s]" 	  (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:hostname" }
String KG_Trockner_IP     		  "SPs02 IP  [%s]" 	        (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:IP" }
String KG_Trockner_DeviceState  "SPs02 Device State"      (gSonoffSw2Info)          { channel="mqtt:topic:KG_Trockner_thg:devicestate" }
 
String KG_Trockner_POW_Time     "Trockner POW Time"                      <time>     (gSonoffSw2)  { channel="mqtt:topic:KG_Trockner_thg:TimePOW" }
Number KG_Trockner_POW_Voltage  "Trockner Spannung [%.0f V]"             <energy>   (gSonoffSw2)  { channel="mqtt:topic:KG_Trockner_thg:Voltage" }
Number KG_Trockner_POW_Power    "Trockner Leistung [%.0f W]"            <energy>   (gSonoffSw2,gSonoffSw2Db)  { channel="mqtt:topic:KG_Trockner_thg:Power" }
// Number KG_Trockner_POW_AppPower "Trockner Scheinleistung [%.0f VA]"      <energy>   (gSonoffSw2)  { channel="mqtt:topic:KG_Trockner_thg:AppPower" }
 
Number KG_Trockner_POW_Total 	  "Trockner Total [%.3f kWh]"              <energy>   (gSonoffSw2,gSonoffSw2Db)  { channel="mqtt:topic:KG_Trockner_thg:Total" }
Number KG_Trockner_POW_Today 	  "Trockner Today [%.3f kWh]"              <energy>   (gSonoffSw2,gSonoffSw2Db)  { channel="mqtt:topic:KG_Trockner_thg:Today" }
Number KG_Trockner_POW_PowerAVG "POW_PowAvg 5-min d. [%.2f Wh]"          <energy>   (gSonoffSw2,gSonoffSw2Db)  { channel="mqtt:topic:KG_Trockner_thg:PowerAVG" }
 
Number KG_Trockner_Daily_POW_Total 	  "Trockner Daily Total [%.3f kWh]"  <energy>   (gSonoffSw2,gSonoffSw2Db,gSonoffDaily)
Number KG_Trockner_Daily_POW_Today 	  "Trockner Daily [%.3f kWh]"        <energy>   (gSonoffSw2,gSonoffSw2Db,gSonoffDaily)
 
// "Start  [ %1$ta, %1$td. %1$tH:%1$tM ]"
String    KG_Trockner_PublicState "Trockner Status (public)"                        (gSonoffSw2Info,gDryerRun) { channel="mqtt:topic:KG_Trockner_thg:Pubstate" }
DateTime  KG_Trockner_Start_Zeit  "Trockner Anfang [ %1$ta, %1$td. %1$tH:%1$tM ]"   <time>     (gSonoffSw2,gDryerRun)
Number    KG_Trockner_Start_kWh   "Trockner Anfang [%.3f kWh]"           <energy>   (gSonoffSw2,gSonoffSw2Db,gDryerRun)
DateTime  KG_Trockner_Ende_Zeit   "Trockner Ende [ %1$ta, %1$td. %1$tH:%1$tM ]"      <time>     (gSonoffSw2,gDryerRun)
Number    KG_Trockner_Ende_kWh    "Trockner Ende [%.3f kWh]"             <energy>   (gSonoffSw2,gSonoffSw2Db,gDryerRun)
Number    KG_Trockner_Dauer  "Trockner Dauer [%.0f min]" 		             <time>     (gSonoffSw2,gSonoffSw2Db,gDryerRun)
Number    KG_Trockner_Verbrauch_kWh   "Trockner Verbrauch [%.3f kWh]"    <energy>   (gSonoffSw2,gSonoffSw2Db,gDryerRun)
Switch    KG_Trockner_Update      "Update"      	                                  (gPersist,gSonoffSw2,gDryerRun)
 
Switch KG_Trockner_SendTelegram "Send Telegram" 	    (gPersist,gDryerRun)
Switch KG_Trockner_SetTime      "Set Time"      	    (gPersist)
DateTime  KG_Trockner_Timestamp "Trockner Timestamp" 		          <time>
Number  KG_Trockner_t_delta     "Trockner t_delta [%.0f min]" 		          <time>
Switch KG_Trockner_Finished       "Finished"      	    (gPersist)
Number KG_Trockner_Finished_Count "Finished count [%d]"      (gPersist,gSonoffDaily)
Number KG_Trockner_Daily_Finished_Count 	 "Daily Finished count [%d]"      (gPersist,gSonoffDaily)
Switch KG_Trockner_Dummy       "Dummy" // in order to define which set of rules will be used for updates
Switch KG_Trockner_Daily_Cron  "Daily - manual update" // in order to trigger daily cronjob manually

sonoff.rules

// sonoffTrockner.rules
// https://neuendorf-online.de/posts/heimautomatisierung/wie-du-mit-einem-sonoff-pow-deine-waschmaschine-smart-machst-teil-2/
// http://discoveration.de/smarthome/waschmaschine-sagt-fertig/1230/
// https://community.openhab.org/t/datetime-conversion/54266
/*
 * ToDo:
 * - write Sensor data to databse: daily, after each run
*/
 
val String filename = "sonoffTrockner.rules"
// val mqttBroker20  = getActions("mqtt","mqtt:broker:MqttPandora20")
// val mqttBroker78  = getActions("mqtt","mqtt:broker:MqttMagicMirror78")
/*
 * if (state != ACTIV , i.e. OFF or STANDBY)
 *      1) if (Power or PowerAVG > active threshold) go to ACTIV, set start values
 *
 * (if state == ACTIV)
 *      2) if (PowerAVG < stop threshold) go to FINISHED, set stop values
 *      3) else (i.e. was ACTIVE and is still ACTIV) update values
*/
/* Trigger TXrockner */
val Number P_AVG_ACTIVE = 15 // Event START
val Number P_ACTIVE = 700   // Event START
val Number P_AVG_STOP = 10 // Event STOP
/* Trigger WXasher
val Number P_AVG_ACTIVE = 20 // Event START
val Number P_ACTIVE = 500   // Event START
val Number P_AVG_STOP = 4 // Event STOP
*/
 
// do not declare getActions here - will cause error
// val actions         = getActions("mqtt","mqtt:systemBroker:embedded-mqtt-broker")
// actions.publishMQTT("test/system/started","true") 
 
val Number MODE_0_OFF = 0
val Number MODE_1_ON = 1
val Number MODE_2_ACTIVE = 2
val Number MODE_3_FINISHED = 3
val Number MODE_4_STANDBY = 4
 
var String opState = "undef"
var Number finishCount = 99
 
var Number  t_begin     = null
var Number  t_end       = null
var Number  t_delta_ms  = null
var Number  t_delta_sec = null
var Number  t_delta_min = null
 
var double kWh_begin    = 0
var double kWh_end      = 0
var double kWh_delta    = 0
var double kWh_total    = 0
 
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner 15min: update status"
when
    // Time cron "0 0/15 * 1/1 * ? *" or //alle 15 min see http://www.cronmaker.com/
    Time cron "0 0/15 * 1/1 * ? *" or  // cronjob at 23:50 see http://www.cronmaker.com/
    Item KG_Trockner_Daily_Cron changed from OFF to ON
then
    if (KG_Trockner_OpState         === null || KG_Trockner_OpState.state       == NULL)   KG_Trockner_OpState.postUpdate(MODE_0_OFF)
    opState = transform("MAP", "sonoffPOW.map", KG_Trockner_OpState.state.toString)    
 
 
    logInfo(filename, "Trockner 15min update state: " + opState + " mqtt: house/tele/trockner/OHstate" )
	// house/tele/trockner/STATE
	// house/tele/trockner/OHstate
	// house/tele/trockner/OHpwr
    //val mqttBrokerTrocknerUpdate78  = getActions("mqtt","mqtt:broker:MqttMagicMirror78")
    val mqttBroker78T1  = getActions("mqtt","mqtt:broker:MqttMagicMirror78")
    mqttBroker78T1.publishMQTT("house/tele/trockner/OHstate", opState ) 
    // // mqttBroker20.publishMQTT("house/tele/trockner/OHstate", opState + " (20)") 
 
    kWh_total  = (KG_Trockner_POW_Total.state as Number).doubleValue
    var Number mqtt_kWh_total = kWh_total
    logInfo(filename, "Trockner 15min update OHprw: " +  mqtt_kWh_total.toString  )
    mqttBroker78T1.publishMQTT("house/tele/trockner/OHpwr", mqtt_kWh_total.toString) 
    // mqttBroker20.publishMQTT("house/tele/trockner/OHpwr", mqtt_kWh_total.toString) 
 
    logInfo(filename, "Trockner 15min update: " + KG_Trockner_POW_Total.state + " / mqtt " + mqtt_kWh_total.toString + " / kw " + kWh_total.toString  )
 
    KG_Trockner_Daily_Cron.postUpdate(OFF)
end
 
 
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner Daily: set counter"
when
    Item T_Input2 changed
then
    // Number T_Input1 --- ok
    logInfo(filename, "Trockner new input 2: " + T_Input2.state )
    var Number t_number = T_Input2.state as DecimalType
    logInfo(filename, "Trockner new count 2: " + t_number.toString )
    KG_Trockner_Finished_Count.postUpdate(t_number)
    KG_Trockner_Daily_Finished_Count.postUpdate(t_number)
end
 
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner Daily: save daily statistics"
when
    // Time cron "0 0/15 * 1/1 * ? *" or //alle 15 min see http://www.cronmaker.com/
    Time cron "0 50 23 1/1 * ? *" or  // cronjob at 23:50 see http://www.cronmaker.com/
    Item KG_Trockner_Daily_Cron changed from OFF to ON
then
    KG_Trockner_Daily_POW_Total.postUpdate(KG_Trockner_Daily_POW_Total.state)
    KG_Trockner_Daily_POW_Today.postUpdate(KG_Trockner_POW_Today.state)
    KG_Trockner_Daily_Finished_Count.postUpdate(KG_Trockner_Finished_Count.state)
    logInfo(filename, "Trockner Daily: Total Power " + KG_Trockner_Daily_POW_Total.state )
 
    logInfo(filename, "Trockner Daily: Total Power switch old> " + KG_Trockner_Switch.state )
    val mqttBrokerTrocknerDaily78  = getActions("mqtt","mqtt:broker:MqttMagicMirror78")
    val mqttBrokerTrocknerDaily20  = getActions("mqtt","mqtt:broker:MqttPandora20")
 
    mqttBrokerTrocknerDaily20.publishMQTT("house/trockner/cmnd/sonoff/Power", "") // send empty message, this will trigger POWER State as response
    logInfo(filename, "Trockner Daily: Total Power switch new> " + KG_Trockner_Switch.state )
 
    mqttBrokerTrocknerDaily78.publishMQTT("house/trockner/stat/sonoff/OHstate", opState ) 
 
    KG_Trockner_Daily_Cron.postUpdate(OFF)
end
 
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner Sim: Power changed"
when
    Item T_Soll_Setpoint changed
then
    if (T_Soll_Setpoint.state === null || T_Soll_Setpoint.state === NULL ) T_Soll_Setpoint.postUpdate(0)
    if (KG_Trockner_Finished_Count.state === null || KG_Trockner_Finished_Count.state === NULL )KG_Trockner_Finished_Count.postUpdate (0)
    if (T_Soll_Setpoint.state != KG_Trockner_POW_Power.state ) {
        KG_Trockner_POW_Power.postUpdate(T_Soll_Setpoint.state)
        var Number POW_pwr = KG_Trockner_POW_Total.state as DecimalType
        POW_pwr = POW_pwr + 0.02
        KG_Trockner_POW_Total.postUpdate(POW_pwr)
        logInfo(filename, "Trockner Sim: Soll " + T_Soll_Setpoint.state + ", Ist: " + KG_Trockner_POW_Power.state + ", Pwr Total: " + KG_Trockner_POW_Total.state  )
    }
end
 
 
 
 
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner UPDATE"
when
    Item KG_Trockner_Update changed from OFF to ON
then
    // logInfo(filename, "Trockner UPDATE Verbrauch: 0 ")
    opState = transform("MAP", "sonoffPOW.map", KG_Trockner_OpState.state.toString)
    // logInfo(filename, "Trockner xdA0 UPDATE time (" + opState + ") Zeit: " + t_begin + " ms  " )
 
    t_end = now.millis
    t_begin  = new DateTime(KG_Trockner_Start_Zeit.state.toString).millis
    // https://community.openhab.org/t/datetime-conversion/54266
 
    if(t_begin === null || t_begin == NULL ||t_begin == 0 )    {t_begin = t_end}
    t_delta_ms =  t_end - t_begin
    t_delta_min = (t_delta_ms / 1000 / 60 ).intValue
    KG_Trockner_Dauer.postUpdate( t_delta_min)
    KG_Trockner_Ende_Zeit.postUpdate( new DateTimeType )
    logInfo(filename, "Trockner xdA UPDATE time (" + opState + ") Zeit: Start " + t_begin + " ms / End " + t_end + " ms / Delta " + t_delta_ms + " ms / Delta " + t_delta_min + " min")
 
 
    kWh_end  = (KG_Trockner_POW_Total.state as Number).doubleValue
    KG_Trockner_Ende_kWh.postUpdate( kWh_end )
    kWh_begin  = (KG_Trockner_Start_kWh.state as Number).doubleValue // updt 12--Dec
    if(kWh_begin == NULL ||kWh_begin == 0 )    {kWh_begin = kWh_end} // updt 12-Dec
    kWh_delta = (kWh_end - kWh_begin)
    KG_Trockner_Verbrauch_kWh.postUpdate( kWh_delta)
    logInfo(filename, "Trockner xdB UPDATE consumption (" + opState + ") Verbrauch:  " + kWh_begin + " kWh / " + kWh_end + " kWh / " + KG_Trockner_Verbrauch_kWh.state)
 
    var Number mqtt_Wh_delta = kWh_delta * 1000 // convert from kWh to Wh
    val mqttBroker20  = getActions("mqtt","mqtt:broker:MqttPandora20")
    mqttBroker20.publishMQTT("house/trockner/stat/sonoff/OHstate", opState + " (20)") 
    mqttBroker20.publishMQTT("house/trockner/stat/sonoff/OHpwr", mqtt_Wh_delta.intValue.toString) 
 
    val mqttBroker78T2  = getActions("mqtt","mqtt:broker:MqttMagicMirror78")
    mqttBroker78T2.publishMQTT("house/trockner/stat/sonoff/OHstate", opState ) 
    mqttBroker78T2.publishMQTT("house/trockner/stat/sonoff/OHpwr", mqtt_Wh_delta.intValue.toString) 
 
    KG_Trockner_Update.postUpdate(OFF)
end
 
/* no updated ....
 * Trigger: Power oder PowerAVG changed
 * 
 * if (state != ACTIV , i.e. OFF or STANDBY)
 *      1) if (Power or PowerAVG > active threshold) go to ACTIV, set start values
 *
 * (if state == ACTIV)
 *      2) if (PowerAVG < stop threshold) go to FINISHED, set stop values
 *      3) else (i.e. was ACTIVE and is still ACTIV) update values
 *
 * (always)
 *      4) if (PowerAVG = 0) go to OFF
*/
//----------------------------------------------------------------------------------------------------------------------
/**
 * Trockner
 * Aus: <= 2W
 * Standby/Fertig: 3-5W
 * Aktiv: Durchschnittsleistung > 0
 */
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner Mode: Power changed"
when
    // Item KG_Trockner_Dummy changed from ON to OFF   // to prevent that this rule is executed
    Item KG_Trockner_POW_Power changed or
    Item KG_Trockner_POW_PowerAVG changed
then
    opState = transform("MAP", "sonoffPOW.map", KG_Trockner_OpState.state.toString)
    // logInfo(filename, "Trockner x0 CHANGE: mode (" + opState + ")")
 
    if(KG_Trockner_OpState.state != MODE_2_ACTIVE ){ // x1
        // was not ACTIVE; test if should get active
        // Event Start      : (Mode != Active) AND (P.AVG > P-AVG.Start ( 7Wh) OR P > P.Active > 400W)
        // logInfo(filename, "Trockner x1-1 CHANGE: mode (" + opState + ") state != ACTIVE")
        // val Number P_AVG_ACTIVE = 7 // Event START
        // val Number P_ACTIVE = 700   // Event START
        if (KG_Trockner_POW_PowerAVG.state > P_AVG_ACTIVE || 
            KG_Trockner_POW_Power.state > P_ACTIVE ) { // 0.2 Watt, Verbraucher aus
            // event: change to ACTIVE
            logInfo(filename, "Trockner x1-2 START: old mode (" + opState + ") event: change to ACTIVE" )
            KG_Trockner_OpState.postUpdate(MODE_2_ACTIVE)
            opState = transform("MAP", "sonoffPOW.map", KG_Trockner_OpState.state.toString)
            KG_Trockner_PublicState.postUpdate("Läuft")
            t_begin    = now.millis 
            KG_Trockner_Timestamp.postUpdate(  new DateTimeType )
            KG_Trockner_Start_Zeit.postUpdate( new DateTimeType )
            kWh_begin  = (KG_Trockner_POW_Total.state as Number).doubleValue
            KG_Trockner_Start_kWh.postUpdate( kWh_begin )
            KG_Trockner_Update.postUpdate( ON ) // This will reset Dauer (time)
            logInfo(filename, "Trockner x1-3 START: mode (" + opState + ") / Power: " + KG_Trockner_POW_Power.state + " Average: " + KG_Trockner_POW_PowerAVG.state)
            T_Debug2.postUpdate("x1-3 Pwr > ACTIVE = ACTIVE" ) 
        }
 
    } else { // x2  Stete == MODE_2_ACTIVE
        // was ACTIVE; test if is now finished
        logInfo(filename, "Trockner x2-1 CHANGE: mode (" + opState + ") state == ACTIVE")
        // val Number P_AVG_STOP = 30 // Event STOP
        if (KG_Trockner_POW_PowerAVG.state < P_AVG_STOP 
            // || KG_Trockner_POW_Power.state == 0 
            ) { 
            // Event FERTIG       : P.AVG < P-AVG.Stop  (30Wh) 
            logInfo(filename, "Trockner x2-2 FINISHED: old mode (" + opState + ") event: change to NOT ACTIVE" )
            KG_Trockner_Timestamp.postUpdate(  new DateTimeType )
            KG_Trockner_Ende_Zeit.postUpdate( new DateTimeType )
            KG_Trockner_Ende_kWh.postUpdate( KG_Trockner_POW_Total.state )
            if (KG_Trockner_POW_Power.state != 0)  { // STANDBY
                KG_Trockner_OpState.postUpdate(MODE_3_FINISHED)
            } else {
                KG_Trockner_OpState.postUpdate(MODE_0_OFF)
            }
            opState = transform("MAP", "sonoffPOW.map", KG_Trockner_OpState.state.toString)
            KG_Trockner_PublicState.postUpdate(opState)
            var Number totalCount = KG_Trockner_Finished_Count.state as DecimalType // updt 12-Dec
            totalCount = totalCount + 1
            KG_Trockner_Finished_Count.postUpdate(totalCount)
            KG_Trockner_SendTelegram.postUpdate(ON)
            logInfo(filename, "Trockner x2-3 FINISHED: new mode (" + opState + ") beendet um: " + KG_Trockner_Ende_Zeit.state)
            T_Debug2.postUpdate("x2-3 FINISHED: Pwr < ACTIVE, Pr.State ACTIVE / new mode (" + opState + ")" ) 
        } else { // machine is still active
            logInfo(filename, "Trockner x2-1 ACTIVE: mode (" + opState + ") request update KG_Trockner_Update ")
            KG_Trockner_Update.postUpdate( ON ) // This will reset Dauer (time)
        }
    } // check ACTIVE or NOT ACTIVE
 
    // To be sure that Status will be set to OFF if machine is OFF regardless of the logic above
    if (KG_Trockner_POW_PowerAVG.state == 0 && KG_Trockner_OpState.state != MODE_0_OFF
        // || KG_Trockner_POW_Power.state == 0  // problem: short drop to zero
        ) { 
        KG_Trockner_OpState.postUpdate(MODE_0_OFF)
        opState = transform("MAP", "sonoffPOW.map", KG_Trockner_OpState.state.toString)
        KG_Trockner_PublicState.postUpdate(opState)
        logInfo(filename, "Trockner x3 OFF: Pwr = 0 OR Pwr-AVG = 0 / new mode (" + opState + ")" ) 
        // T_Debug2.postUpdate("x3 OFF: Pwr = 0 OR Pwr-AVG = 0 / new mode (" + opState + ")" ) 
    }
end
 
 
//----------------------------------------------------------------------------------------------------------------------
/*
val Number MODE_2_ACTIVE = 2
val Number MODE_3_FINISHED = 3
*/
rule "Trockner state: Notify finished"
when
    Item KG_Trockner_OpState changed from MODE_2_ACTIVE to MODE_3_FINISHED or
    Item KG_Trockner_SendTelegram changed from OFF to ON
then
    var String tmpTime = KG_Trockner_Ende_Zeit.state.format("%1$td.%1$tm %1$tH:%1$tM")
    sendTelegram("botWashNDry", "Trockner ist fertig. (" + KG_Trockner_OpState.state + ") " + tmpTime)
    KG_Trockner_SendTelegram.postUpdate(OFF)
    logInfo(filename, "Trockner NACHRICHT: fertig (" + KG_Trockner_OpState.state + ") " + tmpTime)
 
end
 
 
 
//----------------------------------------------------------------------------------------------------------------------
rule "Trockner state: Init"
when
    System started
then
    createTimer(now.plusSeconds(170)) [|
        if (KG_Trockner_OpState         === null || KG_Trockner_OpState.state       == NULL)   KG_Trockner_OpState.postUpdate(MODE_0_OFF)
        if (KG_Trockner_State           === null || KG_Trockner_State.state         == NULL)   KG_Trockner_State.postUpdate(OFF)
        if (KG_Trockner_Switch          === OFF                                            )   KG_Trockner_Switch.postUpdate(OFF)
        if (KG_Trockner_POW_Total       === null || KG_Trockner_POW_Total.state     == NULL)   KG_Trockner_POW_Total.postUpdate(0)
        if (KG_Trockner_POW_Today       === null || KG_Trockner_POW_Today.state     == NULL)   KG_Trockner_POW_Today.postUpdate(0)
        if (KG_Trockner_POW_PowerAVG    === null || KG_Trockner_POW_PowerAVG.state  == NULL)   KG_Trockner_POW_PowerAVG.postUpdate(0)
        if (KG_Trockner_Start_Zeit      === null                                           )   KG_Trockner_Start_Zeit.postUpdate( new DateTimeType )
        if (KG_Trockner_Start_kWh       === null || KG_Trockner_Start_kWh.state     == NULL)   KG_Trockner_Start_kWh.postUpdate(0)
        if (KG_Trockner_Ende_Zeit       === null || KG_Trockner_Ende_Zeit.state     == NULL)   KG_Trockner_Ende_Zeit.postUpdate( new DateTimeType )
        if (KG_Trockner_Ende_kWh        === null || KG_Trockner_Ende_kWh.state      == NULL)   KG_Trockner_Ende_kWh.postUpdate(0)
        if (KG_Trockner_Dauer           === null || KG_Trockner_Dauer.state         == NULL)   KG_Trockner_Dauer.postUpdate(0)
        if (KG_Trockner_Verbrauch_kWh   === null || KG_Trockner_Verbrauch_kWh.state == NULL)   KG_Trockner_Verbrauch_kWh.postUpdate(0)
        if (KG_Trockner_Update          === null || KG_Trockner_Update.state        == NULL)   KG_Trockner_Update.postUpdate(OFF)
        if (KG_Trockner_Finished_Count  === null || KG_Trockner_Finished_Count.state == NULL)   KG_Trockner_Finished_Count.postUpdate(0)
    ]
end