added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.wemo/.classpath
Normal file
32
bundles/org.openhab.binding.wemo/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.wemo/.project
Normal file
23
bundles/org.openhab.binding.wemo/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.wemo</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.wemo/NOTICE
Normal file
13
bundles/org.openhab.binding.wemo/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
273
bundles/org.openhab.binding.wemo/README.md
Normal file
273
bundles/org.openhab.binding.wemo/README.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Belkin Wemo Binding
|
||||
|
||||
This binding integrates the [Belkin WeMo Family](https://www.belkin.com/us/Products/c/home-automation/).
|
||||
The integration happens either through the WeMo-Link bridge, which acts as an IP gateway to the ZigBee devices or through WiFi connection to standalone devices.
|
||||
|
||||
## Supported Things
|
||||
|
||||
The WeMo Binding supports the Socket, Insight, Lightswitch, Motion, Dimmer, Coffemaker and Maker devices, as well as the WeMo-Link bridge with WeMo LED bulbs.
|
||||
The Binding also supports the Crock-Pot Smart Slow Cooker, Mr. Coffee Smart Coffemaker as well as the Holmes Smart Air Purifier, Holmes Smart Humidifier and Holmes Smart Heater.
|
||||
|
||||
## Discovery
|
||||
|
||||
The WeMo devices are discovered through UPnP discovery service in the network. Devices will show up in the inbox and can be easily added as Things.
|
||||
|
||||
## Binding Configuration
|
||||
|
||||
The binding does not need any configuration.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
For manual Thing configuration, one needs to know the UUID of a certain WeMo device.
|
||||
In the thing file, this looks e.g. like
|
||||
|
||||
```
|
||||
wemo:socket:Switch1 [udn="Socket-1_0-221242K11xxxxx"]
|
||||
```
|
||||
|
||||
For a WeMo Link bridge and paired LED Lights, please use the following Thing definition
|
||||
|
||||
```
|
||||
Bridge wemo:bridge:Bridge-1_0-231445B01006A0 [udn="Bridge-1_0-231445B010xxxx"] {
|
||||
MZ100 94103EA2B278xxxx [ deviceID="94103EA2B278xxxx" ]
|
||||
MZ100 94103EA2B278xxxx [ deviceID="94103EA2B278xxxx" ]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Channels
|
||||
|
||||
Devices support some of the following channels:
|
||||
|
||||
| Channel Type | Item Type | Description | Available on Thing |
|
||||
|---------------------|-----------|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| motionDetection | Switch | On if motion is detected, off otherwise. (Motion Sensor only) | Motion |
|
||||
| lastMotionDetected | DateTime | Date and Time when the last motion was detected. (Motion Sensor only) | Motion |
|
||||
| state | Switch | This channel controls the actual binary State of a Device or represents Motion Detection. | All but Dimmer, Crockpot, Airpurifier and Humidifier |
|
||||
| lastChangedAt | DateTime | Date and Time the device was last turned on or of. | Insight |
|
||||
| lastOnFor | Number | Time in seconds an Insight device was last turned on for. | Insight |
|
||||
| onToday | Number | Time in seconds an Insight device has been switched on today. | Insight |
|
||||
| onTotal | Number | Time in seconds an Insight device has been switched on totally. | Insight |
|
||||
| timespan | Number | Time in seconds over which onTotal applies. Typically 2 weeks except first used. | Insight |
|
||||
| averagePower | Number | Average power consumption in Watts. | Insight |
|
||||
| currentPower | Number | Current power consumption of an Insight device. 0 if switched off. | Insight |
|
||||
| energyToday | Number | Energy in Wh used today. | Insight |
|
||||
| energyTotal | Number | Energy in Wh used in total. | Insight |
|
||||
| standbyLimit | Number | Minimum energy draw in W to register device as switched on (default 8W, configurable via WeMo App). | Insight |
|
||||
| onStandBy | Switch | Read-only indication of whether or not the device plugged in to the insight switch is drawing more than the standby limit. | Insight |
|
||||
| relay | Switch | Switches the integrated relay contact close/open | Maker |
|
||||
| sensor | Switch | Shows the state of the integrated sensor | Maker |
|
||||
| coffeeMode | String | Operation mode of a WeMo Coffee Maker | CoffeeMaker |
|
||||
| modeTime | Number | Current amount of time, in minutes, that the Coffee Maker has been in the current mode | CoffeeMaker |
|
||||
| timeRemaining | Number | Remaining brewing time of a WeMo Coffee Maker | CoffeeMaker |
|
||||
| waterLevelReached | Switch | Indicates if the WeMo Coffee Maker needs to be refilled | CoffeeMaker |
|
||||
| cleanAdvise | Switch | Indicates if a WeMo Coffee Maker needs to be cleaned | CoffeeMaker |
|
||||
| filterAdvise | Switch | Indicates if a WeMo Coffee Maker needs to have the filter changed | CoffeeMaker |
|
||||
| brewed | DateTime | Date/time the coffee maker last completed brewing coffee | CoffeeMaker |
|
||||
| lastCleaned | DateTime | Date/time the coffee maker last completed cleaning | CoffeeMaker |
|
||||
| brightness | Number | Brightness of a WeMo LED od Dimmwer. | LED, DimmerSwitch |
|
||||
| faderCountDownTime | Number | Dimmer fading duration time in minutes | DimmerSwitch |
|
||||
| faderEnabled | Switch | Switch the fader ON/OFF | DimmerSwitch |
|
||||
| timerStart | Switch | Switch the fading timer ON/OFF | DimmerSwitch |
|
||||
| nightMode | Switch | Switch the nightMode ON/OFF | DimmerSwitch |
|
||||
| startTime | DateTime | Time when the nightMode starts | DimmerSwitch |
|
||||
| endTime | DateTime | Time when the nightMode ends | DimmerSwitch |
|
||||
| nightModeBrightness | Number | Brightness used in nightMode | DimmerSwitch |
|
||||
| cookMode | String | Shows the operation mode of a WeMo Crockpot (OFF, WARM, LOW, HIGH | Crockpot |
|
||||
| warmCookTime | Number | Shows the timer settings for warm cooking mode | Crockpot |
|
||||
| lowCookTime | Number | Shows the timer settings for low cooking mode | Crockpot |
|
||||
| highCookTime | Number | Shows the timer settings for high cooking mode | Crockpot |
|
||||
| cookedTime | Number | Shows the elapsed cooking time | Crockpot |
|
||||
| purifierMode | String | Runmode of Air Purifier (OFF, LOW, MED, HIGH, AUTO) | Air Purifier |
|
||||
| airQuality | String | Air quality (POOR, MODERATE, GOOD) | Air Purifier |
|
||||
| ionizer | Switch | Indicates whether the ionizer is switched ON or OFF | Air Purifier |
|
||||
| filterLife | Number | Indicates the remaining filter lifetime in Percent | Air Purifier, Humidifier |
|
||||
| expiredFilterTime | Number | Indicates whether the filter lifetime has expired or not | Air Purifier, Humidifier |
|
||||
| filterPresent | Switch | Indicates the presence of an air filter | Air Purifier |
|
||||
| humidifierMode | String | Runmode of Humidifier (OFF, MIN, LOW, MED, HIGH, MAX) | Humidifier |
|
||||
| desiredHumidity | Number | Shows desired humidity in Percent | Humidifier |
|
||||
| currentHumidity | Number | Shows current humidity in Percent | Humidifier |
|
||||
| heaterMode | String | Runmode of Heater (OFF, FROSTPROTECT, HIGH, LOW, ECO) | Heater |
|
||||
| currentTemp | Number | Shows current temperature | Heater |
|
||||
| targetTemp | Number | Shows target temperature | Heater |
|
||||
| autoOffTime | DateTime | Time when the heater switches off | Heater |
|
||||
| heatingRemaining | Number | Shows the remaining heating time | Heater |
|
||||
|
||||
|
||||
## Full Example
|
||||
|
||||
demo.things:
|
||||
|
||||
```
|
||||
wemo:socket:Switch1 "DemoSwitch" @ "Office" [udn="Socket-1_0-221242K11xxxxx"]
|
||||
wemo:motion:Sensor1 "MotionSensor" @ "Entrance" [udn="Sensor-1_0-221337L11xxxxx"]
|
||||
|
||||
Bridge wemo:bridge:Bridge-1_0-231445B010xxxx [udn="Bridge-1_0-231445B010xxxx"] {
|
||||
MZ100 94103EA2B278xxxx "DemoLight1" @ "Living" [ deviceID="94103EA2B278xxxx" ]
|
||||
MZ100 94103EA2B278xxxx "DemoLoght2" @ "Living" [ deviceID="94103EA2B278xxxx" ]
|
||||
}
|
||||
```
|
||||
|
||||
demo.items:
|
||||
|
||||
```
|
||||
// Switch
|
||||
Switch DemoSwitch { channel="wemo:socket:Switch1:state" }
|
||||
|
||||
// Lightswitch
|
||||
Switch LightSwitch { channel="wemo:lightswitch:Lightswitch1:state" }
|
||||
|
||||
// Motion
|
||||
Switch MotionSensor { channel="wemo:Motion:Sensor1:motionDetection" }
|
||||
DateTime MotionDetected { channel="wemo:Motion:Sensor1:lastMotionDetected" }
|
||||
|
||||
// Insight
|
||||
Switch InsightSwitch { channel="wemo:insight:Insight-1_0-xxxxxxxxxxxxxx:state" }
|
||||
Number InsightPower { channel="wemo:insight:Insight-1_0-xxxxxxxxxxxxxx:currentPower" }
|
||||
Number InsightLastOn { channel="wemo:insight:Insight-1_0-xxxxxxxxxxxxxx:lastOnFor" }
|
||||
Number InsightToday { channel="wemo:insight:Insight-1_0-xxxxxxxxxxxxxx:onToday" }
|
||||
Number InsightTotal { channel="wemo:insight:Insight-1_0-xxxxxxxxxxxxxx:onTotal" }
|
||||
|
||||
// LED Bulbs
|
||||
Switch LED1 { channel="wemo:MZ100:Bridge-1_0-231445B010xxxx:94103EA2B278xxxx:brightness" }
|
||||
Dimmer dLED1 { channel="wemo:MZ100:Bridge-1_0-231445B010xxxx:94103EA2B278xxxx:brightness" }
|
||||
Switch LED2 { channel="wemo:MZ100:Bridge-1_0-231445B010xxxx:94103EA2B278xxxx:brightness" }
|
||||
Dimmer dLED2 { channel="wemo:MZ100:Bridge-1_0-231445B010xxxx:94103EA2B278xxxx:brightness" }
|
||||
|
||||
// DimmerSwitch
|
||||
Switch DimmerSwitch { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:brightness" }
|
||||
Dimmer dDimmerSwitch { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:brightness" }
|
||||
Number DimmerSwitchFaderTime { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:faderCountDownTime" }
|
||||
Switch DimmerSwitchFaderOn { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:faderEnabled" }
|
||||
Switch DimmerSwitchTimer { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:timerStart" }
|
||||
Switch DimmerNightMode { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:nightMode" }
|
||||
Dimmer NightModeBrightness { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:nightModeBrightness" }
|
||||
DateTime NightModeStart { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:startTime" }
|
||||
DateTime NightModeEnd { channel="wemo:dimmer:Dimmer-1_0-231445B010xxxx:endTime" }
|
||||
|
||||
// CoffeMaker
|
||||
Switch CoffeSwitch { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:state" }
|
||||
String CoffeMode { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:coffeeMode" }
|
||||
Number CoffeModeTime { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:modeTime" }
|
||||
Number CoffeModeRemaining { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:timeRemaining" }
|
||||
Switch CoffeWater { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:waterLevelReached" }
|
||||
Switch CoffeCleanAdvicse { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:cleanAdvise" }
|
||||
Switch CoffeFilterAdvicse { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:filterAdvise" }
|
||||
DateTime CoffeLastCleaned { channel="wemo:coffee:Coffee-1_0-231445B010xxxx:lastCleaned" }
|
||||
|
||||
// Crockpot
|
||||
String crockpotMode { channel="wemo:crockpot:Crockpot-1_0-231445B010xxxx:cookMode" }
|
||||
Number warmCookTime { channel="wemo:crockpot:Crockpot-1_0-231445B010xxxx:warmCookTime" }
|
||||
Number lowCookTime { channel="wemo:crockpot:Crockpot-1_0-231445B010xxxx:lowCookTime" }
|
||||
Number highCookTime { channel="wemo:crockpot:Crockpot-1_0-231445B010xxxx:highCookTime" }
|
||||
Number cookedTime { channel="wemo:crockpot:Crockpot-1_0-231445B010xxxx:cookedTime" }
|
||||
|
||||
// Air Purifier
|
||||
String airMode { channel="wemo:purifier:AirPurifier-1_0-231445B010xxxx:airMode" }
|
||||
String airQuality { channel="wemo:purifier:AirPurifier-1_0-231445B010xxxx:airQuality" }
|
||||
Switch ionizer { channel="wemo:purifier:AirPurifier-1_0-231445B010xxxx:ionizer" }
|
||||
Number filterLife { channel="wemo:purifier:AirPurifier-1_0-231445B010xxxx:filterLife" }
|
||||
Switch filterExpired { channel="wemo:purifier:AirPurifier-1_0-231445B010xxxx:filterExpired" }
|
||||
Switch filterPresent { channel="wemo:purifier:AirPurifier-1_0-231445B010xxxx:filterPresent" }
|
||||
|
||||
// Humidifier
|
||||
String humidifierMode { channel="wemo:humidifier:Humidifier-1_0-231445B010xxxx:humidifierMode" }
|
||||
Number desiredHumidity { channel="wemo:humidifier:Humidifier-1_0-231445B010xxxx:desiredHumidity" }
|
||||
Number currentHumidity { channel="wemo:humidifier:Humidifier-1_0-231445B010xxxx:currentHumidity" }
|
||||
Number filterLife { channel="wemo:humidifier:Humidifier-1_0-231445B010xxxx:filterLife" }
|
||||
Switch filterExpired { channel="wemo:humidifier:Humidifier-1_0-231445B010xxxx:filterExpired" }
|
||||
String waterLevel { channel="wemo:humidifier:Humidifier-1_0-231445B010xxxx:waterLevel" }
|
||||
|
||||
// Heater
|
||||
String heaterMode { channel="wemo:heater:HeaterB-1_0-231445B010xxxx:heaterMode" }
|
||||
Number currentTemp { channel="wemo:heater:HeaterB-1_0-231445B010xxxx:currentTemp" }
|
||||
Number targetTemp { channel="wemo:heater:HeaterB-1_0-231445B010xxxx:targetTemp" }
|
||||
DateTime autoOffTime { channel="wemo:heater:HeaterB-1_0-231445B010xxxx:autoOffTime" }
|
||||
String heaterRemaining { channel="wemo:heater:HeaterB-1_0-231445B010xxxx:heaterRemaining" }
|
||||
|
||||
```
|
||||
|
||||
demo.sitemap:
|
||||
|
||||
```
|
||||
sitemap demo label="Main Menu"
|
||||
{
|
||||
Frame {
|
||||
// Switch
|
||||
Switch item=DemoSwitch
|
||||
|
||||
// LightSwitch
|
||||
Switch item=LightSwitch
|
||||
|
||||
// Motion
|
||||
Switch item=MotionSensor
|
||||
Text item=MotionSensorLastChanged icon="clock"
|
||||
|
||||
// Insight
|
||||
Switch item=InsightSwitch
|
||||
Number item=InsightPower
|
||||
Number item=InsightLastOn
|
||||
Number item=InsightToday
|
||||
Number item=InsightTotal
|
||||
|
||||
// LED Bulb
|
||||
Switch item=LED1
|
||||
Slider item=dLED1
|
||||
Switch item=LED2
|
||||
Slider item=dLED2
|
||||
|
||||
//DimmerSwitch
|
||||
Switch item=DimmerSwitch
|
||||
Slider item=dDimmerSwitch
|
||||
Number item=DimmerSwitchFaderTime
|
||||
Switch item=DimmerSwitchFaderOn
|
||||
Switch item=DimmerSwitchTimer
|
||||
Switch item=DimmerNightMode
|
||||
Slider item=NightModeBrightness
|
||||
Text item=NightModeStart
|
||||
Text item=NightModeEnd
|
||||
|
||||
// CoffeMaker
|
||||
Switch item=CoffeSwitch
|
||||
Text item=CoffeMode
|
||||
Number item=CoffeModeTime
|
||||
Number item=CoffeModeRemaining
|
||||
Switch item=CoffeWater
|
||||
Switch item=CoffeCleanAdvicse
|
||||
Switch item=CoffeFilterAdvicse
|
||||
DateTime item=CoffeLastCleaned
|
||||
|
||||
// CrockPot
|
||||
Switch item=crockpotMode label="Cooking Mode" mappings=[OFF="OFF", WARM="Warm", LOW="Low", HIGH="High"]
|
||||
Number item=warmCookTime
|
||||
Number item=lowCookTime
|
||||
Number item=highCookTime
|
||||
Number item=cookedTime
|
||||
|
||||
// Air Purifier
|
||||
Switch item=airMode label="Cooking Mode" mappings=[OFF="OFF", LOW="Low", MED="Med", HIGH="High", AUTO="Auto"]
|
||||
Text item=airQuality
|
||||
Switch item=ionizer
|
||||
Number item=filterLive
|
||||
Switch item=filterExpired
|
||||
Switch item=filterPresent
|
||||
|
||||
// Humidifier
|
||||
Switch item=humidifierMode label="Cooking Mode" mappings=[OFF="OFF", MIN="Min", LOW="Low", MED="Med", HIGH="High", MAX="Max"]
|
||||
Number item=desiredHumidity
|
||||
Number item=currentHumidity
|
||||
Number item=filterLive
|
||||
Switch item=filterExpired
|
||||
|
||||
// Heater
|
||||
Switch item=heaterMode label="Heater Mode" mappings=[OFF="OFF", FROSTPROTECT="FrostProtect", HIGH="High", LOW="Low", ECO="Eco"]
|
||||
Number item=currentTemp
|
||||
Setpoint item=targetTemp
|
||||
Text item=autoOffTime
|
||||
Number item=heaterRemaining
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
17
bundles/org.openhab.binding.wemo/pom.xml
Normal file
17
bundles/org.openhab.binding.wemo/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.wemo</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Wemo Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.wemo-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-wemo" description="Wemo Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-upnp</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.wemo/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link WemoBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
* @author Mihir Patil - Added standby switch
|
||||
*/
|
||||
public class WemoBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "wemo";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_SOCKET = new ThingTypeUID(BINDING_ID, "socket");
|
||||
public static final ThingTypeUID THING_TYPE_INSIGHT = new ThingTypeUID(BINDING_ID, "insight");
|
||||
public static final ThingTypeUID THING_TYPE_LIGHTSWITCH = new ThingTypeUID(BINDING_ID, "lightswitch");
|
||||
public static final ThingTypeUID THING_TYPE_MOTION = new ThingTypeUID(BINDING_ID, "motion");
|
||||
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
|
||||
public static final ThingTypeUID THING_TYPE_MZ100 = new ThingTypeUID(BINDING_ID, "MZ100");
|
||||
public static final ThingTypeUID THING_TYPE_MAKER = new ThingTypeUID(BINDING_ID, "Maker");
|
||||
public static final ThingTypeUID THING_TYPE_COFFEE = new ThingTypeUID(BINDING_ID, "CoffeeMaker");
|
||||
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
|
||||
public static final ThingTypeUID THING_TYPE_CROCKPOT = new ThingTypeUID(BINDING_ID, "Crockpot");
|
||||
public static final ThingTypeUID THING_TYPE_PURIFIER = new ThingTypeUID(BINDING_ID, "Purifier");
|
||||
public static final ThingTypeUID THING_TYPE_HUMIDIFIER = new ThingTypeUID(BINDING_ID, "Humidifier");
|
||||
public static final ThingTypeUID THING_TYPE_HEATER = new ThingTypeUID(BINDING_ID, "Heater");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_STATE = "state";
|
||||
public static final String CHANNEL_MOTIONDETECTION = "motionDetection";
|
||||
public static final String CHANNEL_LASTMOTIONDETECTED = "lastMotionDetected";
|
||||
public static final String CHANNEL_LASTCHANGEDAT = "lastChangedAt";
|
||||
public static final String CHANNEL_LASTONFOR = "lastOnFor";
|
||||
public static final String CHANNEL_ONTODAY = "onToday";
|
||||
public static final String CHANNEL_ONTOTAL = "onTotal";
|
||||
public static final String CHANNEL_TIMESPAN = "timespan";
|
||||
public static final String CHANNEL_AVERAGEPOWER = "averagePower";
|
||||
public static final String CHANNEL_CURRENTPOWER = "currentPower";
|
||||
public static final String CHANNEL_ENERGYTODAY = "energyToday";
|
||||
public static final String CHANNEL_ENERGYTOTAL = "energyTotal";
|
||||
public static final String CHANNEL_STANDBYLIMIT = "standByLimit";
|
||||
public static final String CHANNEL_BRIGHTNESS = "brightness";
|
||||
public static final String CHANNEL_RELAY = "relay";
|
||||
public static final String CHANNEL_SENSOR = "sensor";
|
||||
public static final String CHANNEL_ONSTANDBY = "onStandBy";
|
||||
|
||||
public static final String CHANNEL_COFFEEMODE = "coffeeMode";
|
||||
public static final String CHANNEL_MODETIME = "modeTime";
|
||||
public static final String CHANNEL_TIMEREMAINING = "timeRemaining";
|
||||
public static final String CHANNEL_WATERLEVELREACHED = "waterLevelReached";
|
||||
public static final String CHANNEL_CLEANADVISE = "cleanAdvise";
|
||||
public static final String CHANNEL_FILTERADVISE = "filterAdvise";
|
||||
public static final String CHANNEL_BREWED = "brewed";
|
||||
public static final String CHANNEL_LASTCLEANED = "lastCleaned";
|
||||
|
||||
public static final String CHANNEL_FADERENABLED = "faderEnabled";
|
||||
public static final String CHANNEL_TIMERSTART = "timerStart";
|
||||
public static final String CHANNEL_FADERCOUNTDOWNTIME = "faderCountDownTime";
|
||||
public static final String CHANNEL_NIGHTMODE = "nightMode";
|
||||
public static final String CHANNEL_STARTTIME = "startTime";
|
||||
public static final String CHANNEL_ENDTIME = "endTime";
|
||||
public static final String CHANNEL_NIGHTMODEBRIGHTNESS = "nightModeBrightness";
|
||||
|
||||
public static final String CHANNEL_COOKMODE = "cookMode";
|
||||
public static final String CHANNEL_LOWCOOKTIME = "lowCookTime";
|
||||
public static final String CHANNEL_WARMCOOKTIME = "warmCooktime";
|
||||
public static final String CHANNEL_HIGHCOOKTIME = "highCooktime";
|
||||
public static final String CHANNEL_COOKEDTIME = "cookedtime";
|
||||
|
||||
public static final String CHANNEL_PURIFIERMODE = "purifierMode";
|
||||
public static final String CHANNEL_AIRQUALITY = "airQuality";
|
||||
public static final String CHANNEL_IONIZER = "ionizer";
|
||||
public static final String CHANNEL_FILTERLIFE = "filterLife";
|
||||
public static final String CHANNEL_EXPIREDFILTERTIME = "expiredFilterTime";
|
||||
public static final String CHANNEL_FILTERPRESENT = "filterPresent";
|
||||
|
||||
public static final String CHANNEL_HUMIDIFIERMODE = "humidifierMode";
|
||||
public static final String CHANNEL_CURRENTHUMIDITY = "currentHumidity";
|
||||
public static final String CHANNEL_DESIREDHUMIDITY = "desiredHumidity";
|
||||
public static final String CHANNEL_WATERLEVEL = "waterLEvel";
|
||||
|
||||
public static final String CHANNEL_HEATERMODE = "heaterMode";
|
||||
public static final String CHANNEL_CURRENTTEMP = "currentTemperature";
|
||||
public static final String CHANNEL_TARGETTEMP = "targetTemperature";
|
||||
public static final String CHANNEL_AUTOOFFTIME = "autoOffTime";
|
||||
public static final String CHANNEL_HEATINGREMAINING = "heatingRemaining";
|
||||
|
||||
// List of thing configuration properties
|
||||
public static final String UDN = "udn";
|
||||
public static final String DEVICE_ID = "deviceID";
|
||||
public static final String POLLINGINTERVALL = "pollingInterval";
|
||||
|
||||
public static final int SUBSCRIPTION_DURATION = 600;
|
||||
public static final int LINK_DISCOVERY_SERVICE_INITIAL_DELAY = 5;
|
||||
public static final String HTTP_CALL_CONTENT_HEADER = "text/xml; charset=utf-8";
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_LIGHT_THING_TYPES = Collections.singleton(THING_TYPE_MZ100);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES = Collections
|
||||
.unmodifiableSet(Stream.of(THING_TYPE_SOCKET, THING_TYPE_INSIGHT, THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION)
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections
|
||||
.unmodifiableSet(Stream
|
||||
.of(THING_TYPE_SOCKET, THING_TYPE_INSIGHT, THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION,
|
||||
THING_TYPE_BRIDGE, THING_TYPE_MZ100, THING_TYPE_MAKER, THING_TYPE_COFFEE, THING_TYPE_DIMMER,
|
||||
THING_TYPE_CROCKPOT, THING_TYPE_PURIFIER, THING_TYPE_HUMIDIFIER, THING_TYPE_HEATER)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.UDN;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.binding.wemo.internal.discovery.WemoLinkDiscoveryService;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoBridgeHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoCoffeeHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoCrockpotHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoDimmerHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoHolmesHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoLightHandler;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoMakerHandler;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
* @author Kai Kreuzer - some refactoring for performance and simplification
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.wemo")
|
||||
public class WemoHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoHandlerFactory.class);
|
||||
|
||||
private UpnpIOService upnpIOService;
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = WemoBindingConstants.SUPPORTED_THING_TYPES;
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@Override
|
||||
protected ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
if (thingTypeUID != null) {
|
||||
logger.debug("Trying to create a handler for ThingType '{}", thingTypeUID);
|
||||
|
||||
WemoHttpCall wemoHttpcaller = new WemoHttpCall();
|
||||
|
||||
if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_BRIDGE)) {
|
||||
logger.debug("Creating a WemoBridgeHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get(UDN));
|
||||
WemoBridgeHandler handler = new WemoBridgeHandler((Bridge) thing);
|
||||
registerDeviceDiscoveryService(handler, wemoHttpcaller);
|
||||
return handler;
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_MAKER)) {
|
||||
logger.debug("Creating a WemoMakerHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get(UDN));
|
||||
return new WemoMakerHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (WemoBindingConstants.SUPPORTED_DEVICE_THING_TYPES.contains(thing.getThingTypeUID())) {
|
||||
logger.debug("Creating a WemoHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get(UDN));
|
||||
return new WemoHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_COFFEE)) {
|
||||
logger.debug("Creating a WemoCoffeeHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get(UDN));
|
||||
return new WemoCoffeeHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_DIMMER)) {
|
||||
logger.debug("Creating a WemoDimmerHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get("udn"));
|
||||
return new WemoDimmerHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_CROCKPOT)) {
|
||||
logger.debug("Creating a WemoCockpotHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get("udn"));
|
||||
return new WemoCrockpotHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_PURIFIER)) {
|
||||
logger.debug("Creating a WemoHolmesHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get("udn"));
|
||||
return new WemoHolmesHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_HUMIDIFIER)) {
|
||||
logger.debug("Creating a WemoHolmesHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get("udn"));
|
||||
return new WemoHolmesHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_HEATER)) {
|
||||
logger.debug("Creating a WemoHolmesHandler for thing '{}' with UDN '{}'", thing.getUID(),
|
||||
thing.getConfiguration().get("udn"));
|
||||
return new WemoHolmesHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else if (thingTypeUID.equals(WemoBindingConstants.THING_TYPE_MZ100)) {
|
||||
return new WemoLightHandler(thing, upnpIOService, wemoHttpcaller);
|
||||
} else {
|
||||
logger.warn("ThingHandler not found for {}", thingTypeUID);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setUpnpIOService(UpnpIOService upnpIOService) {
|
||||
this.upnpIOService = upnpIOService;
|
||||
}
|
||||
|
||||
protected void unsetUpnpIOService(UpnpIOService upnpIOService) {
|
||||
this.upnpIOService = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void removeHandler(ThingHandler thingHandler) {
|
||||
if (thingHandler instanceof WemoBridgeHandler) {
|
||||
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.remove(thingHandler.getThing().getUID());
|
||||
if (serviceReg != null) {
|
||||
serviceReg.unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void registerDeviceDiscoveryService(WemoBridgeHandler wemoBridgeHandler,
|
||||
WemoHttpCall wemoHttpCaller) {
|
||||
WemoLinkDiscoveryService discoveryService = new WemoLinkDiscoveryService(wemoBridgeHandler, upnpIOService,
|
||||
wemoHttpCaller);
|
||||
this.discoveryServiceRegs.put(wemoBridgeHandler.getThing().getUID(),
|
||||
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.openhab.binding.wemo.internal.WemoBindingConstants;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
|
||||
import org.openhab.core.config.discovery.upnp.internal.UpnpDiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoDiscoveryParticipant} is responsible for discovering new and
|
||||
* removed Wemo devices. It uses the central {@link UpnpDiscoveryService}.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
* @author Kai Kreuzer - some refactoring for performance and simplification
|
||||
*
|
||||
*/
|
||||
@Component(service = UpnpDiscoveryParticipant.class, immediate = true)
|
||||
public class WemoDiscoveryParticipant implements UpnpDiscoveryParticipant {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(WemoDiscoveryParticipant.class);
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return WemoBindingConstants.SUPPORTED_THING_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveryResult createResult(RemoteDevice device) {
|
||||
ThingUID uid = getThingUID(device);
|
||||
if (uid != null) {
|
||||
Map<String, Object> properties = new HashMap<>(2);
|
||||
String label = "WeMo Device";
|
||||
try {
|
||||
label = device.getDetails().getFriendlyName();
|
||||
} catch (Exception e) {
|
||||
// ignore and use default label
|
||||
}
|
||||
properties.put(UDN, device.getIdentity().getUdn().getIdentifierString());
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label)
|
||||
.withRepresentationProperty(UDN).build();
|
||||
|
||||
logger.debug("Created a DiscoveryResult for device '{}' with UDN '{}'",
|
||||
device.getDetails().getFriendlyName(), device.getIdentity().getUdn().getIdentifierString());
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThingUID getThingUID(@Nullable RemoteDevice device) {
|
||||
if (device != null) {
|
||||
if (device.getDetails().getManufacturerDetails().getManufacturer() != null) {
|
||||
if (device.getDetails().getManufacturerDetails().getManufacturer().toUpperCase().contains("BELKIN")) {
|
||||
if (device.getDetails().getModelDetails().getModelName() != null) {
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("socket")) {
|
||||
logger.debug("Discovered a WeMo Socket thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_SOCKET, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("insight")) {
|
||||
logger.debug("Discovered a WeMo Insight thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_INSIGHT,
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase()
|
||||
.startsWith("lightswitch")) {
|
||||
logger.debug("Discovered a WeMo Lightswitch thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_LIGHTSWITCH,
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("motion")) {
|
||||
logger.debug("Discovered a WeMo Motion thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_MOTION, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("sensor")) {
|
||||
logger.debug("Discovered a WeMo Motion thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_MOTION, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("bridge")) {
|
||||
logger.debug("Discovered a WeMo Bridge thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_BRIDGE, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("maker")) {
|
||||
logger.debug("Discovered a WeMo Maker thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_MAKER, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("coffee")) {
|
||||
logger.debug("Discovered a WeMo Coffe Maker thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_COFFEE, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("dimmer")) {
|
||||
logger.debug("Discovered a WeMo Dimmer Switch thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_DIMMER, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("crockpot")) {
|
||||
logger.debug("Discovered a WeMo enabled Crockpot thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_CROCKPOT,
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase()
|
||||
.startsWith("airpurifier")) {
|
||||
logger.debug("Discovered a WeMo enabled Holmes Air Purifier thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_PURIFIER,
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase()
|
||||
.startsWith("humidifier")) {
|
||||
logger.debug("Discovered a WeMo enabled Holmes Humidifier thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_HUMIDIFIER,
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
if (device.getDetails().getModelDetails().getModelName().toLowerCase().startsWith("heater")) {
|
||||
logger.debug("Discovered a WeMo enabled Heater thing with UDN '{}'",
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(THING_TYPE_HEATER, device.getIdentity().getUdn().getIdentifierString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.discovery;
|
||||
|
||||
import org.jupnp.UpnpService;
|
||||
import org.jupnp.model.message.header.RootDeviceHeader;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoDiscoveryService} is a {@link DiscoveryService} implementation, which can find WeMo UPnP devices in
|
||||
* the network.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*
|
||||
*/
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.wemo")
|
||||
public class WemoDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(WemoDiscoveryService.class);
|
||||
|
||||
public WemoDiscoveryService() {
|
||||
super(5);
|
||||
}
|
||||
|
||||
private UpnpService upnpService;
|
||||
|
||||
@Reference
|
||||
protected void setUpnpService(UpnpService upnpService) {
|
||||
this.upnpService = upnpService;
|
||||
}
|
||||
|
||||
protected void unsetUpnpService(UpnpService upnpService) {
|
||||
this.upnpService = null;
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
logger.debug("Starting WeMo UPnP discovery...");
|
||||
startScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
logger.debug("Stopping WeMo UPnP discovery...");
|
||||
stopScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting UPnP RootDevice search...");
|
||||
if (upnpService != null) {
|
||||
upnpService.getControlPoint().search(new RootDeviceHeader());
|
||||
} else {
|
||||
logger.debug("upnpService not set");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
removeOlderResults(getTimestampOfLastScan());
|
||||
super.stopScan();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.handler.WemoBridgeHandler;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
/**
|
||||
* The {@link WemoLinkDiscoveryService} is responsible for discovering new and
|
||||
* removed WeMo devices connected to the WeMo Link Bridge.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class WemoLinkDiscoveryService extends AbstractDiscoveryService implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoLinkDiscoveryService.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MZ100);
|
||||
|
||||
public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]";
|
||||
|
||||
/**
|
||||
* Maximum time to search for devices in seconds.
|
||||
*/
|
||||
private static final int SEARCH_TIME = 20;
|
||||
|
||||
/**
|
||||
* Scan interval for scanning job in seconds.
|
||||
*/
|
||||
private static final int SCAN_INTERVAL = 120;
|
||||
|
||||
/**
|
||||
* The handler for WeMo Link bridge
|
||||
*/
|
||||
private final WemoBridgeHandler wemoBridgeHandler;
|
||||
|
||||
/**
|
||||
* Job which will do the background scanning
|
||||
*/
|
||||
private final WemoLinkScan scanningRunnable;
|
||||
|
||||
/**
|
||||
* Schedule for scanning
|
||||
*/
|
||||
private ScheduledFuture<?> scanningJob;
|
||||
|
||||
/**
|
||||
* The Upnp service
|
||||
*/
|
||||
private UpnpIOService service;
|
||||
|
||||
private final WemoHttpCall wemoHttpCaller;
|
||||
|
||||
public WemoLinkDiscoveryService(WemoBridgeHandler wemoBridgeHandler, UpnpIOService upnpIOService,
|
||||
WemoHttpCall wemoHttpCaller) {
|
||||
super(SEARCH_TIME);
|
||||
this.wemoBridgeHandler = wemoBridgeHandler;
|
||||
|
||||
this.wemoHttpCaller = wemoHttpCaller;
|
||||
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
|
||||
this.scanningRunnable = new WemoLinkScan();
|
||||
if (wemoBridgeHandler == null) {
|
||||
logger.warn("no bridge handler for scan given");
|
||||
}
|
||||
this.activate(null);
|
||||
}
|
||||
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return SUPPORTED_THING_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan() {
|
||||
logger.trace("Starting WeMoEndDevice discovery on WeMo Link {}", wemoBridgeHandler.getThing().getUID());
|
||||
try {
|
||||
String devUDN = "uuid:" + wemoBridgeHandler.getThing().getConfiguration().get(UDN).toString();
|
||||
logger.trace("devUDN = '{}'", devUDN);
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:bridge:1#GetEndDevices\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:GetEndDevices xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DevUDN>" + devUDN
|
||||
+ "</DevUDN><ReqListType>PAIRED_LIST</ReqListType>" + "</u:GetEndDevices>" + "</s:Body>"
|
||||
+ "</s:Envelope>";
|
||||
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
String wemoURL = deviceURL + "/upnp/control/bridge1";
|
||||
|
||||
String endDeviceRequest = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
|
||||
if (endDeviceRequest != null) {
|
||||
logger.trace("endDeviceRequest answered '{}'", endDeviceRequest);
|
||||
|
||||
try {
|
||||
String stringParser = StringUtils.substringBetween(endDeviceRequest, "<DeviceLists>",
|
||||
"</DeviceLists>");
|
||||
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
|
||||
// check if there are already paired devices with WeMo Link
|
||||
if ("0".equals(stringParser)) {
|
||||
logger.debug("There are no devices connected with WeMo Link. Exit discovery");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build parser for received <DeviceList>
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
InputSource is = new InputSource();
|
||||
is.setCharacterStream(new StringReader(stringParser));
|
||||
|
||||
Document doc = db.parse(is);
|
||||
NodeList nodes = doc.getElementsByTagName("DeviceInfo");
|
||||
|
||||
// iterate the devices
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Element element = (Element) nodes.item(i);
|
||||
|
||||
NodeList deviceIndex = element.getElementsByTagName("DeviceIndex");
|
||||
Element line = (Element) deviceIndex.item(0);
|
||||
logger.trace("DeviceIndex: {}", getCharacterDataFromElement(line));
|
||||
|
||||
NodeList deviceID = element.getElementsByTagName("DeviceID");
|
||||
line = (Element) deviceID.item(0);
|
||||
String endDeviceID = getCharacterDataFromElement(line);
|
||||
logger.trace("DeviceID: {}", endDeviceID);
|
||||
|
||||
NodeList friendlyName = element.getElementsByTagName("FriendlyName");
|
||||
line = (Element) friendlyName.item(0);
|
||||
String endDeviceName = getCharacterDataFromElement(line);
|
||||
logger.trace("FriendlyName: {}", endDeviceName);
|
||||
|
||||
NodeList vendor = element.getElementsByTagName("Manufacturer");
|
||||
line = (Element) vendor.item(0);
|
||||
String endDeviceVendor = getCharacterDataFromElement(line);
|
||||
logger.trace("Manufacturer: {}", endDeviceVendor);
|
||||
|
||||
NodeList model = element.getElementsByTagName("ModelCode");
|
||||
line = (Element) model.item(0);
|
||||
String endDeviceModelID = getCharacterDataFromElement(line);
|
||||
endDeviceModelID = endDeviceModelID.replaceAll(NORMALIZE_ID_REGEX, "_");
|
||||
|
||||
logger.trace("ModelCode: {}", endDeviceModelID);
|
||||
|
||||
if (SUPPORTED_THING_TYPES.contains(new ThingTypeUID(BINDING_ID, endDeviceModelID))) {
|
||||
logger.debug("Discovered a WeMo LED Light thing with ID '{}'", endDeviceID);
|
||||
|
||||
ThingUID bridgeUID = wemoBridgeHandler.getThing().getUID();
|
||||
ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, endDeviceModelID);
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_MZ100)) {
|
||||
String thingLightId = endDeviceID;
|
||||
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, thingLightId);
|
||||
|
||||
Map<String, Object> properties = new HashMap<>(1);
|
||||
properties.put(DEVICE_ID, endDeviceID);
|
||||
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
|
||||
.withProperties(properties)
|
||||
.withBridge(wemoBridgeHandler.getThing().getUID()).withLabel(endDeviceName)
|
||||
.build();
|
||||
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Discovered an unsupported device :");
|
||||
logger.debug("DeviceIndex : {}", getCharacterDataFromElement(line));
|
||||
logger.debug("DeviceID : {}", endDeviceID);
|
||||
logger.debug("FriendlyName: {}", endDeviceName);
|
||||
logger.debug("Manufacturer: {}", endDeviceVendor);
|
||||
logger.debug("ModelCode : {}", endDeviceModelID);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse endDevices for bridge '{}'",
|
||||
wemoBridgeHandler.getThing().getUID(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get endDevices for bridge '{}'", wemoBridgeHandler.getThing().getUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.trace("Start WeMo device background discovery");
|
||||
|
||||
if (scanningJob == null || scanningJob.isCancelled()) {
|
||||
this.scanningJob = scheduler.scheduleWithFixedDelay(this.scanningRunnable,
|
||||
LINK_DISCOVERY_SERVICE_INITIAL_DELAY, SCAN_INTERVAL, TimeUnit.SECONDS);
|
||||
} else {
|
||||
logger.trace("scanningJob active");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stop WeMo device background discovery");
|
||||
|
||||
if (scanningJob != null && !scanningJob.isCancelled()) {
|
||||
scanningJob.cancel(true);
|
||||
scanningJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.wemoBridgeHandler.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
|
||||
public static String getCharacterDataFromElement(Element e) {
|
||||
Node child = e.getFirstChild();
|
||||
if (child instanceof CharacterData) {
|
||||
CharacterData cd = (CharacterData) child;
|
||||
return cd.getData();
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
public class WemoLinkScan implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
startScan();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Stefan Triller
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractWemoHandler extends BaseThingHandler {
|
||||
|
||||
public AbstractWemoHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
protected WemoHttpCall wemoHttpCaller;
|
||||
|
||||
public void setWemoHttpCaller(WemoHttpCall wemoHttpCaller) {
|
||||
this.wemoHttpCaller = wemoHttpCaller;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link WemoBridgeHandler} is the handler for a wemo bridge and connects it to
|
||||
* the framework.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*/
|
||||
public class WemoBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoBridgeHandler.class);
|
||||
|
||||
public WemoBridgeHandler(Bridge bridge) {
|
||||
super(bridge);
|
||||
logger.debug("Creating a WemoBridgeHandler for thing '{}'", getThing().getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing WemoBridgeHandler");
|
||||
|
||||
Configuration configuration = getConfig();
|
||||
|
||||
if (configuration.get(UDN) != null) {
|
||||
logger.trace("Initializing WemoBridgeHandler for UDN '{}'", configuration.get(UDN));
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoBridgeHandler. UDN not set.");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
// Not needed, all commands are handled in the {@link WemoLightHandler}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
/**
|
||||
* The {@link WemoCoffeeHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels and to update their states.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
* @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
|
||||
*/
|
||||
|
||||
public class WemoCoffeeHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoCoffeeHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_COFFEE);
|
||||
|
||||
private Map<String, Boolean> subscriptionState = new HashMap<>();
|
||||
|
||||
protected static final int SUBSCRIPTION_DURATION = 600;
|
||||
|
||||
private UpnpIOService service;
|
||||
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private final int REFRESH_INTERVAL = 60;
|
||||
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Runnable refreshRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!isUpnpDeviceRegistered()) {
|
||||
logger.debug("WeMo UPnP device {} not yet registered", getUDN());
|
||||
}
|
||||
|
||||
updateWemoState();
|
||||
onSubscription();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public WemoCoffeeHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
|
||||
super(thing);
|
||||
|
||||
this.wemoHttpCaller = wemoHttpcaller;
|
||||
|
||||
logger.debug("Creating a WemoCoffeeHandler V0.4 for thing '{}'", getThing().getUID());
|
||||
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Configuration configuration = getConfig();
|
||||
|
||||
if (configuration.get("udn") != null) {
|
||||
logger.debug("Initializing WemoCoffeeHandler for UDN '{}'", configuration.get("udn"));
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoCoffeeHandler. UDN not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WeMoCoffeeHandler disposed.");
|
||||
|
||||
removeSubscription();
|
||||
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
try {
|
||||
updateWemoState();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
}
|
||||
} else if (channelUID.getId().equals(CHANNEL_STATE)) {
|
||||
if (command instanceof OnOffType) {
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
|
||||
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
|
||||
+ "<attributeList><attribute><name>Brewed</name><value>NULL</value></attribute>"
|
||||
+ "<attribute><name>LastCleaned</name><value>NULL</value></attribute><attribute>"
|
||||
+ "<name>ModeTime</name><value>NULL</value></attribute><attribute><name>Brewing</name>"
|
||||
+ "<value>NULL</value></attribute><attribute><name>TimeRemaining</name><value>NULL</value>"
|
||||
+ "</attribute><attribute><name>WaterLevelReached</name><value>NULL</value></attribute><"
|
||||
+ "attribute><name>Mode</name><value>4</value></attribute><attribute><name>CleanAdvise</name>"
|
||||
+ "<value>NULL</value></attribute><attribute><name>FilterAdvise</name><value>NULL</value></attribute>"
|
||||
+ "<attribute><name>Cleaning</name><value>NULL</value></attribute></attributeList>"
|
||||
+ "</u:SetAttributes>" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
String wemoURL = getWemoURL("deviceevent");
|
||||
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
updateState(CHANNEL_STATE, OnOffType.ON);
|
||||
State newMode = new StringType("Brewing");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
// if command.equals(OnOffType.OFF) we do nothing because WeMo Coffee Maker cannot be switched off
|
||||
// remotely
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
|
||||
subscriptionState.put(service, succeeded);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
// We can subscribe to GENA events, but there is no usefull response right now.
|
||||
}
|
||||
|
||||
private synchronized void onSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Checking WeMo GENA subscription for '{}'", this);
|
||||
|
||||
String subscription = "deviceevent1";
|
||||
if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
|
||||
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(subscription, true);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeSubscription() {
|
||||
logger.debug("Removing WeMo GENA subscription for '{}'", this);
|
||||
|
||||
if (service.isRegistered(this)) {
|
||||
String subscription = "deviceevent1";
|
||||
if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
|
||||
service.removeSubscription(this, subscription);
|
||||
}
|
||||
|
||||
subscriptionState = new HashMap<>();
|
||||
service.unregisterParticipant(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = REFRESH_INTERVAL;
|
||||
Object refreshConfig = config.get("pollingInterval");
|
||||
if (refreshConfig != null) {
|
||||
refreshInterval = ((BigDecimal) refreshConfig).intValue();
|
||||
logger.debug("Setting WemoCoffeeHandler refreshInterval to '{}' seconds", refreshInterval);
|
||||
}
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUpnpDeviceRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link updateWemoState} polls the actual state of a WeMo CoffeeMaker.
|
||||
*/
|
||||
protected void updateWemoState() {
|
||||
String action = "GetAttributes";
|
||||
String actionService = "deviceevent";
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
try {
|
||||
String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
|
||||
"</attributeList>");
|
||||
|
||||
// Due to Belkins bad response formatting, we need to run this twice.
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
|
||||
logger.trace("CoffeeMaker response '{}' for device '{}' received", stringParser,
|
||||
getThing().getUID());
|
||||
|
||||
stringParser = "<data>" + stringParser + "</data>";
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
InputSource is = new InputSource();
|
||||
is.setCharacterStream(new StringReader(stringParser));
|
||||
|
||||
Document doc = db.parse(is);
|
||||
NodeList nodes = doc.getElementsByTagName("attribute");
|
||||
|
||||
// iterate the attributes
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Element element = (Element) nodes.item(i);
|
||||
|
||||
NodeList deviceIndex = element.getElementsByTagName("name");
|
||||
Element line = (Element) deviceIndex.item(0);
|
||||
String attributeName = getCharacterDataFromElement(line);
|
||||
logger.trace("attributeName: {}", attributeName);
|
||||
|
||||
NodeList deviceID = element.getElementsByTagName("value");
|
||||
line = (Element) deviceID.item(0);
|
||||
String attributeValue = getCharacterDataFromElement(line);
|
||||
logger.trace("attributeValue: {}", attributeValue);
|
||||
|
||||
switch (attributeName) {
|
||||
case "Mode":
|
||||
State newMode = new StringType("Brewing");
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
updateState(CHANNEL_STATE, OnOffType.ON);
|
||||
newMode = new StringType("Refill");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "1":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("PlaceCarafe");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "2":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("RefillWater");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "3":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("Ready");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "4":
|
||||
updateState(CHANNEL_STATE, OnOffType.ON);
|
||||
newMode = new StringType("Brewing");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "5":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("Brewed");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "6":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("CleaningBrewing");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "7":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("CleaningSoaking");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
case "8":
|
||||
updateState(CHANNEL_STATE, OnOffType.OFF);
|
||||
newMode = new StringType("BrewFailCarafeRemoved");
|
||||
updateState(CHANNEL_COFFEEMODE, newMode);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "ModeTime":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = new DecimalType(attributeValue);
|
||||
updateState(CHANNEL_MODETIME, newAttributeValue);
|
||||
}
|
||||
break;
|
||||
case "TimeRemaining":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = new DecimalType(attributeValue);
|
||||
updateState(CHANNEL_TIMEREMAINING, newAttributeValue);
|
||||
}
|
||||
break;
|
||||
case "WaterLevelReached":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = new DecimalType(attributeValue);
|
||||
updateState(CHANNEL_WATERLEVELREACHED, newAttributeValue);
|
||||
}
|
||||
break;
|
||||
case "CleanAdvise":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF
|
||||
: OnOffType.ON;
|
||||
updateState(CHANNEL_CLEANADVISE, newAttributeValue);
|
||||
}
|
||||
break;
|
||||
case "FilterAdvise":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = attributeValue.equals("0") ? OnOffType.OFF
|
||||
: OnOffType.ON;
|
||||
updateState(CHANNEL_FILTERADVISE, newAttributeValue);
|
||||
}
|
||||
break;
|
||||
case "Brewed":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = getDateTimeState(attributeValue);
|
||||
if (newAttributeValue != null) {
|
||||
updateState(CHANNEL_BREWED, newAttributeValue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "LastCleaned":
|
||||
if (attributeValue != null) {
|
||||
State newAttributeValue = getDateTimeState(attributeValue);
|
||||
if (newAttributeValue != null) {
|
||||
updateState(CHANNEL_LASTCLEANED, newAttributeValue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse attributeList for WeMo CoffeMaker '{}'", this.getThing().getUID(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public State getDateTimeState(String attributeValue) {
|
||||
if (attributeValue != null) {
|
||||
long value = 0;
|
||||
try {
|
||||
value = Long.parseLong(attributeValue) * 1000; // convert s to ms
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
|
||||
getThing().getUID());
|
||||
return null;
|
||||
}
|
||||
ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(value),
|
||||
TimeZone.getDefault().toZoneId());
|
||||
State dateTimeState = new DateTimeType(zoned);
|
||||
if (dateTimeState != null) {
|
||||
logger.trace("New attribute brewed '{}' received", dateTimeState);
|
||||
return dateTimeState;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getWemoURL(String actionService) {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
String wemoURL = null;
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
|
||||
return wemoURL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getCharacterDataFromElement(Element e) {
|
||||
Node child = e.getFirstChild();
|
||||
if (child instanceof CharacterData) {
|
||||
CharacterData cd = (CharacterData) child;
|
||||
return cd.getData();
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoCrockpotHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels and to update their states.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution;
|
||||
*/
|
||||
|
||||
public class WemoCrockpotHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoCrockpotHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_CROCKPOT);
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private static final int DEFAULT_REFRESH_INTERVAL_SECONDS = 120;
|
||||
private final Map<String, Boolean> subscriptionState = new HashMap<>();
|
||||
private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private UpnpIOService service;
|
||||
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Runnable refreshRunnable = () -> {
|
||||
updateWemoState();
|
||||
if (!isUpnpDeviceRegistered()) {
|
||||
logger.debug("WeMo UPnP device {} not yet registered", getUDN());
|
||||
} else {
|
||||
onSubscription();
|
||||
}
|
||||
};
|
||||
|
||||
public WemoCrockpotHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
|
||||
super(thing);
|
||||
|
||||
this.wemoHttpCaller = wemohttpCaller;
|
||||
|
||||
logger.debug("Creating a WemoCrockpotHandler for thing '{}'", getThing().getUID());
|
||||
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Configuration configuration = getConfig();
|
||||
|
||||
if (configuration.get("udn") != null) {
|
||||
logger.debug("Initializing WemoCrockpotHandler for UDN '{}'", configuration.get("udn"));
|
||||
service.registerParticipant(this);
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoCrockpotHandler. UDN not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WeMoCrockpotHandler disposed.");
|
||||
|
||||
removeSubscription();
|
||||
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
String mode = "0";
|
||||
String time = null;
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
updateWemoState();
|
||||
} else if (CHANNEL_COOKMODE.equals(channelUID.getId())) {
|
||||
String commandString = command.toString();
|
||||
switch (commandString) {
|
||||
case "OFF":
|
||||
mode = "0";
|
||||
time = "0";
|
||||
break;
|
||||
case "WARM":
|
||||
mode = "50";
|
||||
break;
|
||||
case "LOW":
|
||||
mode = "51";
|
||||
break;
|
||||
case "HIGH":
|
||||
mode = "52";
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetCrockpotState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<mode>"
|
||||
+ mode + "</mode>" + "<time>" + time + "</time>" + "</u:SetCrockpotState>" + "</s:Body>"
|
||||
+ "</s:Envelope>";
|
||||
String wemoURL = getWemoURL("basicevent");
|
||||
|
||||
if (wemoURL != null) {
|
||||
wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
|
||||
subscriptionState.put(service, succeeded);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
|
||||
this.getThing().getUID());
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
this.stateMap.put(variable, value);
|
||||
}
|
||||
|
||||
private synchronized void onSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Checking WeMo GENA subscription for '{}'", this);
|
||||
|
||||
String subscription = "basicevent1";
|
||||
|
||||
if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
|
||||
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(subscription, true);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeSubscription() {
|
||||
logger.debug("Removing WeMo GENA subscription for '{}'", this);
|
||||
|
||||
if (service.isRegistered(this)) {
|
||||
String subscription = "basicevent1";
|
||||
|
||||
if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
|
||||
service.removeSubscription(this, subscription);
|
||||
}
|
||||
|
||||
subscriptionState.remove(subscription);
|
||||
service.unregisterParticipant(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = DEFAULT_REFRESH_INTERVAL_SECONDS;
|
||||
Object refreshConfig = config.get("refresh");
|
||||
refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVAL_SECONDS
|
||||
: ((BigDecimal) refreshConfig).intValue();
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUpnpDeviceRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link updateWemoState} polls the actual state of a WeMo device and
|
||||
* calls {@link onValueReceived} to update the statemap and channels..
|
||||
*
|
||||
*/
|
||||
protected void updateWemoState() {
|
||||
String action = "GetCrockpotState";
|
||||
String actionService = "basicevent";
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
|
||||
String mode = StringUtils.substringBetween(wemoCallResponse, "<mode>", "</mode>");
|
||||
String time = StringUtils.substringBetween(wemoCallResponse, "<time>", "</time>");
|
||||
String coockedTime = StringUtils.substringBetween(wemoCallResponse, "<coockedTime>",
|
||||
"</coockedTime>");
|
||||
|
||||
if (mode != null && time != null && coockedTime != null) {
|
||||
State newMode = new StringType(mode);
|
||||
State newCoockedTime = DecimalType.valueOf(coockedTime);
|
||||
switch (mode) {
|
||||
case "0":
|
||||
newMode = new StringType("OFF");
|
||||
break;
|
||||
case "50":
|
||||
newMode = new StringType("WARM");
|
||||
State warmTime = DecimalType.valueOf(time);
|
||||
updateState(CHANNEL_WARMCOOKTIME, warmTime);
|
||||
break;
|
||||
case "51":
|
||||
newMode = new StringType("LOW");
|
||||
State lowTime = DecimalType.valueOf(time);
|
||||
updateState(CHANNEL_LOWCOOKTIME, lowTime);
|
||||
break;
|
||||
case "52":
|
||||
newMode = new StringType("HIGH");
|
||||
State highTime = DecimalType.valueOf(time);
|
||||
updateState(CHANNEL_HIGHCOOKTIME, highTime);
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_COOKMODE, newMode);
|
||||
updateState(CHANNEL_COOKEDTIME, newCoockedTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
public String getWemoURL(String actionService) {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
String wemoURL = null;
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
|
||||
return wemoURL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoDimmerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels and to update their states.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*/
|
||||
|
||||
public class WemoDimmerHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoDimmerHandler.class);
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DIMMER);
|
||||
private Map<String, Boolean> subscriptionState = new HashMap<>();
|
||||
private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
protected static final int SUBSCRIPTION_DURATION = 600;
|
||||
private UpnpIOService service;
|
||||
private int currentBrightness;
|
||||
private int currentNightModeBrightness;
|
||||
private String currentNightModeState = null;
|
||||
/**
|
||||
* Set dimming stepsize to 5%
|
||||
*/
|
||||
private static final int DIM_STEPSIZE = 5;
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private int DEFAULT_REFRESH_INTERVAL = 60;
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
private Runnable refreshRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!isUpnpDeviceRegistered()) {
|
||||
logger.debug("WeMo UPnP device {} not yet registered", getUDN());
|
||||
}
|
||||
updateWemoState();
|
||||
onSubscription();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll : {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public WemoDimmerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
|
||||
super(thing);
|
||||
this.wemoHttpCaller = wemohttpCaller;
|
||||
logger.debug("Creating a WemoDimmerHandler for thing '{}'", getThing().getUID());
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Configuration configuration = getConfig();
|
||||
if (configuration.get("udn") != null) {
|
||||
logger.debug("Initializing WemoDimmerHandler for UDN '{}'", configuration.get("udn"));
|
||||
service.registerParticipant(this);
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoDimmerHandler. UDN not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WeMoDimmerHandler disposed.");
|
||||
removeSubscription();
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
if (command instanceof RefreshType) {
|
||||
try {
|
||||
updateWemoState();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
}
|
||||
} else {
|
||||
String action = "SetBinaryState";
|
||||
String argument = "BinaryState";
|
||||
String value = "0";
|
||||
String timeStamp = null;
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_BRIGHTNESS:
|
||||
if (command instanceof OnOffType) {
|
||||
value = command.equals(OnOffType.OFF) ? "0" : "1";
|
||||
setBinaryState(action, argument, value);
|
||||
if (command.equals(OnOffType.OFF)) {
|
||||
State brightnessState = new PercentType("0");
|
||||
updateState(CHANNEL_BRIGHTNESS, brightnessState);
|
||||
updateState(CHANNEL_TIMERSTART, OnOffType.OFF);
|
||||
} else {
|
||||
State brightnessState = new PercentType(currentBrightness);
|
||||
updateState(CHANNEL_BRIGHTNESS, brightnessState);
|
||||
}
|
||||
} else if (command instanceof PercentType) {
|
||||
int newBrightness = ((PercentType) command).intValue();
|
||||
value = String.valueOf(newBrightness);
|
||||
currentBrightness = newBrightness;
|
||||
argument = "brightness";
|
||||
if (value.equals("0")) {
|
||||
value = "1";
|
||||
argument = "brightness";
|
||||
setBinaryState(action, argument, "1");
|
||||
value = "0";
|
||||
argument = "BinaryState";
|
||||
setBinaryState(action, argument, "0");
|
||||
} else if (this.stateMap.get("BinaryState").equals("0")) {
|
||||
argument = "BinaryState";
|
||||
setBinaryState(action, argument, "1");
|
||||
}
|
||||
argument = "brightness";
|
||||
setBinaryState(action, argument, value);
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
int newBrightness;
|
||||
switch (command.toString()) {
|
||||
case "INCREASE":
|
||||
newBrightness = currentBrightness + DIM_STEPSIZE;
|
||||
if (newBrightness > 100) {
|
||||
newBrightness = 100;
|
||||
}
|
||||
value = String.valueOf(newBrightness);
|
||||
currentBrightness = newBrightness;
|
||||
break;
|
||||
case "DECREASE":
|
||||
newBrightness = currentBrightness - DIM_STEPSIZE;
|
||||
if (newBrightness < 0) {
|
||||
newBrightness = 0;
|
||||
}
|
||||
value = String.valueOf(newBrightness);
|
||||
currentBrightness = newBrightness;
|
||||
break;
|
||||
}
|
||||
argument = "brightness";
|
||||
if (value.equals("0")) {
|
||||
value = "1";
|
||||
argument = "brightness";
|
||||
setBinaryState(action, argument, "1");
|
||||
value = "0";
|
||||
argument = "BinaryState";
|
||||
setBinaryState(action, argument, "0");
|
||||
} else if (this.stateMap.get("BinaryState").equals("0")) {
|
||||
argument = "BinaryState";
|
||||
setBinaryState(action, argument, "1");
|
||||
}
|
||||
argument = "brightness";
|
||||
setBinaryState(action, argument, value);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_FADERCOUNTDOWNTIME:
|
||||
argument = "Fader";
|
||||
if (command instanceof DecimalType) {
|
||||
int commandValue = Integer.valueOf(String.valueOf(command));
|
||||
commandValue = commandValue * 60;
|
||||
String commandString = String.valueOf(commandValue);
|
||||
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
|
||||
+ "<brightness></brightness>" + "<fader>" + commandString + ":-1:1:0:0</fader>"
|
||||
+ "<UDN></UDN>";
|
||||
setBinaryState(action, argument, value);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_FADERENABLED:
|
||||
argument = "Fader";
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
|
||||
+ "<brightness></brightness>" + "<fader>600:-1:1:0:0</fader>" + "<UDN></UDN>";
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
|
||||
+ "<brightness></brightness>" + "<fader>600:-1:0:0:0</fader>" + "<UDN></UDN>";
|
||||
}
|
||||
setBinaryState(action, argument, value);
|
||||
break;
|
||||
case CHANNEL_TIMERSTART:
|
||||
argument = "Fader";
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
timeStamp = String.valueOf(ts);
|
||||
logger.info("timestamp '{}' created", timeStamp);
|
||||
String faderSeconds = null;
|
||||
String faderEnabled = null;
|
||||
String[] splitFader = this.stateMap.get("fader").split(":");
|
||||
if (splitFader[0] != null) {
|
||||
faderSeconds = splitFader[0];
|
||||
}
|
||||
if (splitFader[0] != null) {
|
||||
faderEnabled = splitFader[2];
|
||||
}
|
||||
if (faderSeconds != null && faderEnabled != null) {
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
|
||||
+ "<brightness></brightness>" + "<fader>" + faderSeconds + ":" + timeStamp + ":"
|
||||
+ faderEnabled + ":0:0</fader>" + "<UDN></UDN>";
|
||||
updateState(CHANNEL_STATE, OnOffType.ON);
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
value = "<BinaryState></BinaryState>" + "<Duration></Duration>" + "<EndAction></EndAction>"
|
||||
+ "<brightness></brightness>" + "<fader>" + faderSeconds + ":-1:" + faderEnabled
|
||||
+ ":0:0</fader>" + "<UDN></UDN>";
|
||||
}
|
||||
}
|
||||
setBinaryState(action, argument, value);
|
||||
break;
|
||||
case CHANNEL_NIGHTMODE:
|
||||
action = "ConfigureNightMode";
|
||||
argument = "NightModeConfiguration";
|
||||
String nightModeBrightness = String.valueOf(currentNightModeBrightness);
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
value = "<startTime>0</startTime> \\n<nightMode>1</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
|
||||
+ nightModeBrightness + "</nightModeBrightness> \\n";
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
value = "<startTime>0</startTime> \\n<nightMode>0</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
|
||||
+ nightModeBrightness + "</nightModeBrightness> \\n";
|
||||
}
|
||||
setBinaryState(action, argument, value);
|
||||
break;
|
||||
case CHANNEL_NIGHTMODEBRIGHTNESS:
|
||||
action = "ConfigureNightMode";
|
||||
argument = "NightModeConfiguration";
|
||||
if (command instanceof PercentType) {
|
||||
int newBrightness = ((PercentType) command).intValue();
|
||||
String newNightModeBrightness = String.valueOf(newBrightness);
|
||||
value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
|
||||
+ "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
|
||||
+ newNightModeBrightness + "</nightModeBrightness> \\n";
|
||||
currentNightModeBrightness = newBrightness;
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
int newBrightness;
|
||||
String newNightModeBrightness = null;
|
||||
switch (command.toString()) {
|
||||
case "INCREASE":
|
||||
newBrightness = currentNightModeBrightness + DIM_STEPSIZE;
|
||||
if (newBrightness > 100) {
|
||||
newBrightness = 100;
|
||||
}
|
||||
newNightModeBrightness = String.valueOf(newBrightness);
|
||||
currentBrightness = newBrightness;
|
||||
break;
|
||||
case "DECREASE":
|
||||
newBrightness = currentNightModeBrightness - DIM_STEPSIZE;
|
||||
if (newBrightness < 0) {
|
||||
newBrightness = 0;
|
||||
}
|
||||
newNightModeBrightness = String.valueOf(newBrightness);
|
||||
currentNightModeBrightness = newBrightness;
|
||||
break;
|
||||
}
|
||||
value = "<startTime>0</startTime> \\n<nightMode>" + currentNightModeState
|
||||
+ "</nightMode> \\n<endTime>23400</endTime> \\n<nightModeBrightness>"
|
||||
+ newNightModeBrightness + "</nightModeBrightness> \\n";
|
||||
}
|
||||
setBinaryState(action, argument, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
|
||||
subscriptionState.put(service, succeeded);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
|
||||
new Object[] { variable, value, service, this.getThing().getUID() });
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
this.stateMap.put(variable, value);
|
||||
switch (variable) {
|
||||
case "BinaryState":
|
||||
State state = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
|
||||
updateState(CHANNEL_BRIGHTNESS, state);
|
||||
if (state.equals(OnOffType.OFF)) {
|
||||
updateState(CHANNEL_TIMERSTART, OnOffType.OFF);
|
||||
}
|
||||
break;
|
||||
case "brightness":
|
||||
logger.debug("brightness '{}' for device '{}' received", value, getThing().getUID());
|
||||
int newBrightnessValue = Integer.valueOf(value);
|
||||
State newBrightnessState = new PercentType(newBrightnessValue);
|
||||
if (this.stateMap.get("BinaryState").equals("1")) {
|
||||
updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
|
||||
}
|
||||
currentBrightness = newBrightnessValue;
|
||||
break;
|
||||
case "fader":
|
||||
logger.debug("fader '{}' for device '{}' received", value, getThing().getUID());
|
||||
String[] splitFader = value.split(":");
|
||||
if (splitFader[0] != null) {
|
||||
int faderSeconds = Integer.valueOf(splitFader[0]);
|
||||
State faderMinutes = new DecimalType(faderSeconds / 60);
|
||||
logger.debug("faderTime '{} minutes' for device '{}' received", faderMinutes, getThing().getUID());
|
||||
updateState(CHANNEL_FADERCOUNTDOWNTIME, faderMinutes);
|
||||
}
|
||||
if (splitFader[1] != null) {
|
||||
State isTimerRunning = splitFader[1].equals("-1") ? OnOffType.OFF : OnOffType.ON;
|
||||
logger.debug("isTimerRunning '{}' for device '{}' received", isTimerRunning, getThing().getUID());
|
||||
updateState(CHANNEL_TIMERSTART, isTimerRunning);
|
||||
if (isTimerRunning.equals(OnOffType.ON)) {
|
||||
updateState(CHANNEL_STATE, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
if (splitFader[2] != null) {
|
||||
State isFaderEnabled = splitFader[1].equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
logger.debug("isFaderEnabled '{}' for device '{}' received", isFaderEnabled, getThing().getUID());
|
||||
updateState(CHANNEL_FADERENABLED, isFaderEnabled);
|
||||
}
|
||||
break;
|
||||
case "nightMode":
|
||||
State nightModeState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
currentNightModeState = value;
|
||||
logger.debug("nightModeState '{}' for device '{}' received", nightModeState, getThing().getUID());
|
||||
updateState(CHANNEL_NIGHTMODE, nightModeState);
|
||||
break;
|
||||
case "startTime":
|
||||
State startTimeState = getDateTimeState(value);
|
||||
logger.debug("startTimeState '{}' for device '{}' received", startTimeState, getThing().getUID());
|
||||
updateState(CHANNEL_STARTTIME, startTimeState);
|
||||
break;
|
||||
case "endTime":
|
||||
State endTimeState = getDateTimeState(value);
|
||||
logger.debug("endTimeState '{}' for device '{}' received", endTimeState, getThing().getUID());
|
||||
updateState(CHANNEL_ENDTIME, endTimeState);
|
||||
break;
|
||||
case "nightModeBrightness":
|
||||
int nightModeBrightnessValue = Integer.valueOf(value);
|
||||
currentNightModeBrightness = nightModeBrightnessValue;
|
||||
State nightModeBrightnessState = new PercentType(nightModeBrightnessValue);
|
||||
logger.debug("nightModeBrightnessState '{}' for device '{}' received", nightModeBrightnessState,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_NIGHTMODEBRIGHTNESS, nightModeBrightnessState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Checking WeMo GENA subscription for '{}'", this);
|
||||
String subscription = "basicevent1";
|
||||
if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
|
||||
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(subscription, true);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeSubscription() {
|
||||
logger.debug("Removing WeMo GENA subscription for '{}'", this);
|
||||
if (service.isRegistered(this)) {
|
||||
String subscription = "basicevent1";
|
||||
if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
|
||||
service.removeSubscription(this, subscription);
|
||||
}
|
||||
subscriptionState = new HashMap<>();
|
||||
service.unregisterParticipant(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = DEFAULT_REFRESH_INTERVAL;
|
||||
Object refreshConfig = config.get("refresh");
|
||||
if (refreshConfig != null) {
|
||||
refreshInterval = ((BigDecimal) refreshConfig).intValue();
|
||||
}
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 10, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUpnpDeviceRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link updateWemoState} polls the actual state of a WeMo device and
|
||||
* calls {@link onValueReceived} to update the statemap and channels..
|
||||
*
|
||||
*/
|
||||
protected void updateWemoState() {
|
||||
String action = "GetBinaryState";
|
||||
String variable = null;
|
||||
String actionService = "basicevent";
|
||||
String value = null;
|
||||
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
|
||||
if (value != null) {
|
||||
variable = "BinaryState";
|
||||
logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<brightness>", "</brightness>");
|
||||
if (value != null) {
|
||||
variable = "brightness";
|
||||
logger.trace("New brightness '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<fader>", "</fader>");
|
||||
if (value != null) {
|
||||
variable = "fader";
|
||||
logger.trace("New fader value '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
action = "GetNightModeConfiguration";
|
||||
variable = null;
|
||||
value = null;
|
||||
soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
logger.trace("GetNightModeConfiguration response '{}' for device '{}' received", wemoCallResponse,
|
||||
getThing().getUID());
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<startTime>", "</startTime>");
|
||||
if (value != null) {
|
||||
variable = "startTime";
|
||||
logger.trace("New startTime '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<endTime>", "</endTime>");
|
||||
if (value != null) {
|
||||
variable = "endTime";
|
||||
logger.trace("New endTime '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<nightMode>", "</nightMode>");
|
||||
if (value != null) {
|
||||
variable = "nightMode";
|
||||
logger.trace("New nightMode state '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<nightModeBrightness>",
|
||||
"</nightModeBrightness>");
|
||||
if (value != null) {
|
||||
variable = "nightModeBrightness";
|
||||
logger.trace("New nightModeBrightness '{}' for device '{}' received", value,
|
||||
getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed to get actual NightMode state for device '{}': {}", getThing().getUID(),
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
public String getWemoURL(String actionService) {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
String wemoURL = null;
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
|
||||
return wemoURL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public State getDateTimeState(String attributeValue) {
|
||||
if (attributeValue != null) {
|
||||
long value = 0;
|
||||
try {
|
||||
value = Long.parseLong(attributeValue) * 1000; // convert s to ms
|
||||
} catch (NumberFormatException e) {
|
||||
logger.warn("Unable to parse attributeValue '{}' for device '{}'; expected long", attributeValue,
|
||||
getThing().getUID());
|
||||
return null;
|
||||
}
|
||||
ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(value),
|
||||
TimeZone.getDefault().toZoneId());
|
||||
State dateTimeState = new DateTimeType(zoned);
|
||||
if (dateTimeState != null) {
|
||||
logger.trace("New attribute '{}' received", dateTimeState);
|
||||
return dateTimeState;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBinaryState(String action, String argument, String value) {
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:basicevent:1\">" + "<" + argument
|
||||
+ ">" + value + "</" + argument + ">" + "</u:" + action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
String wemoURL = getWemoURL("basicevent");
|
||||
if (wemoURL != null) {
|
||||
logger.trace("About to send content to Dimmer {}", content);
|
||||
wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void setTimerStart(String action, String argument, String value) {
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">" + value
|
||||
+ "</u:SetBinaryState>" + "</s:Body>" + "</s:Envelope>";
|
||||
String wemoURL = getWemoURL("basicevent");
|
||||
if (wemoURL != null) {
|
||||
logger.trace("About to send content to Dimmer {}", content);
|
||||
wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed to set binaryState '{}' for device '{}': {}", value, getThing().getUID(),
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels and to update their states.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution; Added support for WeMo Insight energy measurement
|
||||
* @author Kai Kreuzer - some refactoring for performance and simplification
|
||||
* @author Stefan Bußweiler - Added new thing status handling
|
||||
* @author Erdoan Hadzhiyusein - Adapted the class to work with the new DateTimeType
|
||||
* @author Mihir Patil - Added standby switch
|
||||
*/
|
||||
|
||||
public class WemoHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
|
||||
.of(THING_TYPE_SOCKET, THING_TYPE_INSIGHT, THING_TYPE_LIGHTSWITCH, THING_TYPE_MOTION)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
private Map<String, Boolean> subscriptionState = new HashMap<>();
|
||||
|
||||
private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
// protected static final int SUBSCRIPTION_DURATION = WemoBindingConstants.SUBSCRIPTION_DURATION;
|
||||
|
||||
private UpnpIOService service;
|
||||
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private final int DEFAULT_REFRESH_INTERVAL = 120;
|
||||
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Runnable refreshRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!isUpnpDeviceRegistered()) {
|
||||
logger.debug("WeMo UPnP device {} not yet registered", getUDN());
|
||||
}
|
||||
|
||||
updateWemoState();
|
||||
onSubscription();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public WemoHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
|
||||
super(thing);
|
||||
|
||||
this.wemoHttpCaller = wemohttpCaller;
|
||||
|
||||
logger.debug("Creating a WemoHandler for thing '{}'", getThing().getUID());
|
||||
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Configuration configuration = getConfig();
|
||||
|
||||
if (configuration.get("udn") != null) {
|
||||
logger.debug("Initializing WemoHandler for UDN '{}'", configuration.get("udn"));
|
||||
service.registerParticipant(this);
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoHandler. UDN not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WeMoHandler disposed.");
|
||||
|
||||
removeSubscription();
|
||||
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
try {
|
||||
updateWemoState();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
}
|
||||
} else if (channelUID.getId().equals(CHANNEL_STATE)) {
|
||||
if (command instanceof OnOffType) {
|
||||
try {
|
||||
String binaryState = null;
|
||||
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
binaryState = "1";
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
binaryState = "0";
|
||||
}
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
|
||||
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
|
||||
+ "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
|
||||
+ "</s:Envelope>";
|
||||
|
||||
String wemoURL = getWemoURL("basicevent");
|
||||
|
||||
if (wemoURL != null) {
|
||||
wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send command '{}' for device '{}': {}", command, getThing().getUID(),
|
||||
e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
|
||||
subscriptionState.put(service, succeeded);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'",
|
||||
new Object[] { variable, value, service, this.getThing().getUID() });
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
|
||||
this.stateMap.put(variable, value);
|
||||
|
||||
if (getThing().getThingTypeUID().getId().equals("insight")) {
|
||||
String insightParams = stateMap.get("InsightParams");
|
||||
|
||||
if (insightParams != null) {
|
||||
String[] splitInsightParams = insightParams.split("\\|");
|
||||
|
||||
if (splitInsightParams[0] != null) {
|
||||
OnOffType binaryState = null;
|
||||
binaryState = splitInsightParams[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
if (binaryState != null) {
|
||||
logger.trace("New InsightParam binaryState '{}' for device '{}' received", binaryState,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_STATE, binaryState);
|
||||
}
|
||||
}
|
||||
|
||||
long lastChangedAt = 0;
|
||||
try {
|
||||
lastChangedAt = Long.parseLong(splitInsightParams[1]) * 1000; // convert s to ms
|
||||
} catch (NumberFormatException e) {
|
||||
logger.error("Unable to parse lastChangedAt value '{}' for device '{}'; expected long",
|
||||
splitInsightParams[1], getThing().getUID());
|
||||
}
|
||||
ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(lastChangedAt),
|
||||
TimeZone.getDefault().toZoneId());
|
||||
|
||||
State lastChangedAtState = new DateTimeType(zoned);
|
||||
if (lastChangedAt != 0) {
|
||||
logger.trace("New InsightParam lastChangedAt '{}' for device '{}' received", lastChangedAtState,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_LASTCHANGEDAT, lastChangedAtState);
|
||||
}
|
||||
|
||||
State lastOnFor = DecimalType.valueOf(splitInsightParams[2]);
|
||||
if (lastOnFor != null) {
|
||||
logger.trace("New InsightParam lastOnFor '{}' for device '{}' received", lastOnFor,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_LASTONFOR, lastOnFor);
|
||||
}
|
||||
|
||||
State onToday = DecimalType.valueOf(splitInsightParams[3]);
|
||||
if (onToday != null) {
|
||||
logger.trace("New InsightParam onToday '{}' for device '{}' received", onToday,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_ONTODAY, onToday);
|
||||
}
|
||||
|
||||
State onTotal = DecimalType.valueOf(splitInsightParams[4]);
|
||||
if (onTotal != null) {
|
||||
logger.trace("New InsightParam onTotal '{}' for device '{}' received", onTotal,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_ONTOTAL, onTotal);
|
||||
}
|
||||
|
||||
State timespan = DecimalType.valueOf(splitInsightParams[5]);
|
||||
if (timespan != null) {
|
||||
logger.trace("New InsightParam timespan '{}' for device '{}' received", timespan,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_TIMESPAN, timespan);
|
||||
}
|
||||
|
||||
State averagePower = DecimalType.valueOf(splitInsightParams[6]); // natively given in W
|
||||
if (averagePower != null) {
|
||||
logger.trace("New InsightParam averagePower '{}' for device '{}' received", averagePower,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_AVERAGEPOWER, averagePower);
|
||||
}
|
||||
|
||||
BigDecimal currentMW = new BigDecimal(splitInsightParams[7]);
|
||||
State currentPower = new DecimalType(currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
|
||||
// mW to W
|
||||
if (currentPower != null) {
|
||||
logger.trace("New InsightParam currentPower '{}' for device '{}' received", currentPower,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_CURRENTPOWER, currentPower);
|
||||
}
|
||||
|
||||
BigDecimal energyTodayMWMin = new BigDecimal(splitInsightParams[8]);
|
||||
// recalculate mW-mins to Wh
|
||||
State energyToday = new DecimalType(
|
||||
energyTodayMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
|
||||
if (energyToday != null) {
|
||||
logger.trace("New InsightParam energyToday '{}' for device '{}' received", energyToday,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_ENERGYTODAY, energyToday);
|
||||
}
|
||||
|
||||
BigDecimal energyTotalMWMin = new BigDecimal(splitInsightParams[9]);
|
||||
// recalculate mW-mins to Wh
|
||||
State energyTotal = new DecimalType(
|
||||
energyTotalMWMin.divide(new BigDecimal(60000), RoundingMode.HALF_UP));
|
||||
if (energyTotal != null) {
|
||||
logger.trace("New InsightParam energyTotal '{}' for device '{}' received", energyTotal,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_ENERGYTOTAL, energyTotal);
|
||||
}
|
||||
|
||||
BigDecimal standByLimitMW = new BigDecimal(splitInsightParams[10]);
|
||||
State standByLimit = new DecimalType(standByLimitMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP)); // recalculate
|
||||
// mW to W
|
||||
if (standByLimit != null) {
|
||||
logger.trace("New InsightParam standByLimit '{}' for device '{}' received", standByLimit,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_STANDBYLIMIT, standByLimit);
|
||||
}
|
||||
if (currentMW.divide(new BigDecimal(1000), RoundingMode.HALF_UP).intValue() > standByLimitMW
|
||||
.divide(new BigDecimal(1000), RoundingMode.HALF_UP).intValue()) {
|
||||
updateState(CHANNEL_ONSTANDBY, OnOffType.OFF);
|
||||
} else {
|
||||
updateState(CHANNEL_ONSTANDBY, OnOffType.ON);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
State state = stateMap.get("BinaryState").equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
|
||||
logger.debug("State '{}' for device '{}' received", state, getThing().getUID());
|
||||
|
||||
if (state != null) {
|
||||
if (getThing().getThingTypeUID().getId().equals("motion")) {
|
||||
updateState(CHANNEL_MOTIONDETECTION, state);
|
||||
if (state.equals(OnOffType.ON)) {
|
||||
State lastMotionDetected = new DateTimeType();
|
||||
updateState(CHANNEL_LASTMOTIONDETECTED, lastMotionDetected);
|
||||
}
|
||||
} else {
|
||||
updateState(CHANNEL_STATE, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Checking WeMo GENA subscription for '{}'", this);
|
||||
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
String subscription = "basicevent1";
|
||||
|
||||
if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
|
||||
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(subscription, true);
|
||||
}
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
|
||||
subscription = "insight1";
|
||||
if ((subscriptionState.get(subscription) == null)
|
||||
|| !subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(),
|
||||
subscription);
|
||||
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(subscription, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeSubscription() {
|
||||
logger.debug("Removing WeMo GENA subscription for '{}'", this);
|
||||
|
||||
if (service.isRegistered(this)) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
String subscription = "basicevent1";
|
||||
|
||||
if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
|
||||
service.removeSubscription(this, subscription);
|
||||
}
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_INSIGHT)) {
|
||||
subscription = "insight1";
|
||||
if ((subscriptionState.get(subscription) != null)
|
||||
&& subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
|
||||
service.removeSubscription(this, subscription);
|
||||
}
|
||||
}
|
||||
subscriptionState = new HashMap<>();
|
||||
service.unregisterParticipant(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = DEFAULT_REFRESH_INTERVAL;
|
||||
Object refreshConfig = config.get("refresh");
|
||||
if (refreshConfig != null) {
|
||||
refreshInterval = ((BigDecimal) refreshConfig).intValue();
|
||||
}
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUpnpDeviceRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link updateWemoState} polls the actual state of a WeMo device and
|
||||
* calls {@link onValueReceived} to update the statemap and channels..
|
||||
*
|
||||
*/
|
||||
protected void updateWemoState() {
|
||||
String action = "GetBinaryState";
|
||||
String variable = "BinaryState";
|
||||
String actionService = "basicevent";
|
||||
String value = null;
|
||||
|
||||
if (getThing().getThingTypeUID().getId().equals("insight")) {
|
||||
action = "GetInsightParams";
|
||||
variable = "InsightParams";
|
||||
actionService = "insight";
|
||||
}
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
|
||||
if (variable.equals("InsightParams")) {
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<InsightParams>", "</InsightParams>");
|
||||
} else {
|
||||
value = StringUtils.substringBetween(wemoCallResponse, "<BinaryState>", "</BinaryState>");
|
||||
}
|
||||
if (value != null) {
|
||||
logger.trace("New state '{}' for device '{}' received", value, getThing().getUID());
|
||||
this.onValueReceived(variable, value, actionService + "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get actual state for device '{}': {}", getThing().getUID(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String getWemoURL(String actionService) {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
int portCheckStart = 49151;
|
||||
int portCheckStop = 49157;
|
||||
String wemoURL = null;
|
||||
String host = null;
|
||||
String port = null;
|
||||
if (descriptorURL != null) {
|
||||
host = StringUtils.substringBetween(descriptorURL.toString(), "://", ":");
|
||||
for (int i = portCheckStart; i < portCheckStop; i++) {
|
||||
try {
|
||||
boolean portFound = servicePing(host, i);
|
||||
if (portFound) {
|
||||
logger.trace("WeMo device {} responded at Port {}", getUDN(), i);
|
||||
port = String.valueOf(i);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
wemoURL = "http://" + host + ":" + port + "/upnp/control/" + actionService + "1";
|
||||
logger.trace("WeMo url {}", wemoURL);
|
||||
return wemoURL;
|
||||
}
|
||||
return wemoURL;
|
||||
}
|
||||
|
||||
public boolean servicePing(String host, int port) {
|
||||
logger.trace("Ping WeMo device at '{}:{}'", host, port);
|
||||
try {
|
||||
HttpUtil.executeUrl("GET", "http://" + host + ":" + port, 250);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* The {@link WemoHolmesHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels and to update their states.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution;
|
||||
*/
|
||||
|
||||
public class WemoHolmesHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoHolmesHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_PURIFIER);
|
||||
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private static final int DEFAULT_REFRESH_INTERVAL_SECONDS = 120;
|
||||
private static final int FILTER_LIFE_DAYS = 330;
|
||||
private static final int FILTER_LIFE_MINS = FILTER_LIFE_DAYS * 24 * 60;
|
||||
private final Map<String, Boolean> subscriptionState = new HashMap<>();
|
||||
private final Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private UpnpIOService service;
|
||||
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Runnable refreshRunnable = () -> {
|
||||
if (!isUpnpDeviceRegistered()) {
|
||||
logger.debug("WeMo UPnP device {} not yet registered", getUDN());
|
||||
} else {
|
||||
updateWemoState();
|
||||
onSubscription();
|
||||
}
|
||||
};
|
||||
|
||||
public WemoHolmesHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemohttpCaller) {
|
||||
super(thing);
|
||||
|
||||
this.wemoHttpCaller = wemohttpCaller;
|
||||
|
||||
logger.debug("Creating a WemoHolmesHandler for thing '{}'", getThing().getUID());
|
||||
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Configuration configuration = getConfig();
|
||||
|
||||
if (configuration.get("udn") != null) {
|
||||
logger.debug("Initializing WemoHolmesHandler for UDN '{}'", configuration.get("udn"));
|
||||
service.registerParticipant(this);
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoHolmesHandler. UDN not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WemoHolmesHandler disposed.");
|
||||
|
||||
removeSubscription();
|
||||
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
|
||||
String attribute = null;
|
||||
String value = null;
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
updateWemoState();
|
||||
} else if (CHANNEL_PURIFIERMODE.equals(channelUID.getId())) {
|
||||
attribute = "Mode";
|
||||
String commandString = command.toString();
|
||||
switch (commandString) {
|
||||
case "OFF":
|
||||
value = "0";
|
||||
break;
|
||||
case "LOW":
|
||||
value = "1";
|
||||
break;
|
||||
case "MED":
|
||||
value = "2";
|
||||
break;
|
||||
case "HIGH":
|
||||
value = "3";
|
||||
break;
|
||||
case "AUTO":
|
||||
value = "4";
|
||||
break;
|
||||
}
|
||||
} else if (CHANNEL_IONIZER.equals(channelUID.getId())) {
|
||||
attribute = "Ionizer";
|
||||
if (OnOffType.ON.equals(command)) {
|
||||
value = "1";
|
||||
} else if (OnOffType.OFF.equals(command)) {
|
||||
value = "0";
|
||||
}
|
||||
} else if (CHANNEL_HUMIDIFIERMODE.equals(channelUID.getId())) {
|
||||
attribute = "FanMode";
|
||||
String commandString = command.toString();
|
||||
switch (commandString) {
|
||||
case "OFF":
|
||||
value = "0";
|
||||
break;
|
||||
case "MIN":
|
||||
value = "1";
|
||||
break;
|
||||
case "LOW":
|
||||
value = "2";
|
||||
break;
|
||||
case "MED":
|
||||
value = "3";
|
||||
break;
|
||||
case "HIGH":
|
||||
value = "4";
|
||||
break;
|
||||
case "MAX":
|
||||
value = "5";
|
||||
break;
|
||||
}
|
||||
} else if (CHANNEL_DESIREDHUMIDITY.equals(channelUID.getId())) {
|
||||
attribute = "DesiredHumidity";
|
||||
String commandString = command.toString();
|
||||
switch (commandString) {
|
||||
case "45":
|
||||
value = "0";
|
||||
break;
|
||||
case "50":
|
||||
value = "1";
|
||||
break;
|
||||
case "55":
|
||||
value = "2";
|
||||
break;
|
||||
case "60":
|
||||
value = "3";
|
||||
break;
|
||||
case "100":
|
||||
value = "4";
|
||||
break;
|
||||
}
|
||||
} else if (CHANNEL_HEATERMODE.equals(channelUID.getId())) {
|
||||
attribute = "Mode";
|
||||
String commandString = command.toString();
|
||||
switch (commandString) {
|
||||
case "OFF":
|
||||
value = "0";
|
||||
break;
|
||||
case "FROSTPROTECT":
|
||||
value = "1";
|
||||
break;
|
||||
case "HIGH":
|
||||
value = "2";
|
||||
break;
|
||||
case "LOW":
|
||||
value = "3";
|
||||
break;
|
||||
case "ECO":
|
||||
value = "4";
|
||||
break;
|
||||
}
|
||||
} else if (CHANNEL_TARGETTEMP.equals(channelUID.getId())) {
|
||||
attribute = "SetTemperature";
|
||||
value = command.toString();
|
||||
}
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:deviceevent:1#SetAttributes\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetAttributes xmlns:u=\"urn:Belkin:service:deviceevent:1\">"
|
||||
+ "<attributeList><attribute><name>" + attribute + "</name><value>" + value
|
||||
+ "</value></attribute></attributeList>" + "</u:SetAttributes>" + "</s:Body>"
|
||||
+ "</s:Envelope>";
|
||||
String wemoURL = getWemoURL("deviceevent");
|
||||
|
||||
if (wemoURL != null) {
|
||||
wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Failed to send command '{}' for device '{}':", command, getThing().getUID(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
logger.debug("WeMo {}: Subscription to service {} {}", getUDN(), service, succeeded ? "succeeded" : "failed");
|
||||
subscriptionState.put(service, succeeded);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
logger.debug("Received pair '{}':'{}' (service '{}') for thing '{}'", variable, value, service,
|
||||
this.getThing().getUID());
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
this.stateMap.put(variable, value);
|
||||
}
|
||||
|
||||
private synchronized void onSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Checking WeMo GENA subscription for '{}'", this);
|
||||
|
||||
String subscription = "basicevent1";
|
||||
|
||||
if ((subscriptionState.get(subscription) == null) || !subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), subscription);
|
||||
service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(subscription, true);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeSubscription() {
|
||||
logger.debug("Removing WeMo GENA subscription for '{}'", this);
|
||||
|
||||
if (service.isRegistered(this)) {
|
||||
String subscription = "basicevent1";
|
||||
|
||||
if ((subscriptionState.get(subscription) != null) && subscriptionState.get(subscription).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), subscription);
|
||||
service.removeSubscription(this, subscription);
|
||||
}
|
||||
|
||||
subscriptionState.remove(subscription);
|
||||
service.unregisterParticipant(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = DEFAULT_REFRESH_INTERVAL_SECONDS;
|
||||
Object refreshConfig = config.get("refresh");
|
||||
refreshInterval = refreshConfig == null ? DEFAULT_REFRESH_INTERVAL_SECONDS
|
||||
: ((BigDecimal) refreshConfig).intValue();
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUpnpDeviceRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link updateWemoState} polls the actual state of a WeMo device and
|
||||
* calls {@link onValueReceived} to update the statemap and channels..
|
||||
*
|
||||
*/
|
||||
protected void updateWemoState() {
|
||||
String action = "GetAttributes";
|
||||
String actionService = "deviceevent";
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
logger.trace("State response '{}' for device '{}' received", wemoCallResponse, getThing().getUID());
|
||||
|
||||
String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
|
||||
"</attributeList>");
|
||||
|
||||
// Due to Belkins bad response formatting, we need to run this twice.
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
|
||||
logger.trace("AirPurifier response '{}' for device '{}' received", stringParser,
|
||||
getThing().getUID());
|
||||
|
||||
stringParser = "<data>" + stringParser + "</data>";
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
InputSource is = new InputSource();
|
||||
is.setCharacterStream(new StringReader(stringParser));
|
||||
|
||||
Document doc = db.parse(is);
|
||||
NodeList nodes = doc.getElementsByTagName("attribute");
|
||||
|
||||
// iterate the attributes
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Element element = (Element) nodes.item(i);
|
||||
|
||||
NodeList deviceIndex = element.getElementsByTagName("name");
|
||||
Element line = (Element) deviceIndex.item(0);
|
||||
String attributeName = getCharacterDataFromElement(line);
|
||||
logger.trace("attributeName: {}", attributeName);
|
||||
|
||||
NodeList deviceID = element.getElementsByTagName("value");
|
||||
line = (Element) deviceID.item(0);
|
||||
String attributeValue = getCharacterDataFromElement(line);
|
||||
logger.trace("attributeValue: {}", attributeValue);
|
||||
|
||||
State newMode = new StringType();
|
||||
switch (attributeName) {
|
||||
case "Mode":
|
||||
if ("purifier".equals(getThing().getThingTypeUID().getId())) {
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = new StringType("OFF");
|
||||
break;
|
||||
case "1":
|
||||
newMode = new StringType("LOW");
|
||||
break;
|
||||
case "2":
|
||||
newMode = new StringType("MED");
|
||||
break;
|
||||
case "3":
|
||||
newMode = new StringType("HIGH");
|
||||
break;
|
||||
case "4":
|
||||
newMode = new StringType("AUTO");
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_PURIFIERMODE, newMode);
|
||||
} else {
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = new StringType("OFF");
|
||||
break;
|
||||
case "1":
|
||||
newMode = new StringType("FROSTPROTECT");
|
||||
break;
|
||||
case "2":
|
||||
newMode = new StringType("HIGH");
|
||||
break;
|
||||
case "3":
|
||||
newMode = new StringType("LOW");
|
||||
break;
|
||||
case "4":
|
||||
newMode = new StringType("ECO");
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_HEATERMODE, newMode);
|
||||
}
|
||||
break;
|
||||
case "Ionizer":
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = OnOffType.OFF;
|
||||
break;
|
||||
case "1":
|
||||
newMode = OnOffType.ON;
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_IONIZER, newMode);
|
||||
break;
|
||||
case "AirQuality":
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = new StringType("POOR");
|
||||
break;
|
||||
case "1":
|
||||
newMode = new StringType("MODERATE");
|
||||
break;
|
||||
case "2":
|
||||
newMode = new StringType("GOOD");
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_AIRQUALITY, newMode);
|
||||
break;
|
||||
case "FilterLife":
|
||||
int filterLife = Integer.valueOf(attributeValue);
|
||||
if ("purifier".equals(getThing().getThingTypeUID().getId())) {
|
||||
filterLife = Math.round((filterLife / FILTER_LIFE_MINS) * 100);
|
||||
} else {
|
||||
filterLife = Math.round((filterLife / 60480) * 100);
|
||||
}
|
||||
updateState(CHANNEL_FILTERLIFE, new PercentType(String.valueOf(filterLife)));
|
||||
break;
|
||||
case "ExpiredFilterTime":
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = OnOffType.OFF;
|
||||
break;
|
||||
case "1":
|
||||
newMode = OnOffType.ON;
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_EXPIREDFILTERTIME, newMode);
|
||||
break;
|
||||
case "FilterPresent":
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = OnOffType.OFF;
|
||||
break;
|
||||
case "1":
|
||||
newMode = OnOffType.ON;
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_FILTERPRESENT, newMode);
|
||||
break;
|
||||
case "FANMode":
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = new StringType("OFF");
|
||||
break;
|
||||
case "1":
|
||||
newMode = new StringType("LOW");
|
||||
break;
|
||||
case "2":
|
||||
newMode = new StringType("MED");
|
||||
break;
|
||||
case "3":
|
||||
newMode = new StringType("HIGH");
|
||||
break;
|
||||
case "4":
|
||||
newMode = new StringType("AUTO");
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_PURIFIERMODE, newMode);
|
||||
break;
|
||||
case "DesiredHumidity":
|
||||
switch (attributeValue) {
|
||||
case "0":
|
||||
newMode = new PercentType("45");
|
||||
break;
|
||||
case "1":
|
||||
newMode = new PercentType("50");
|
||||
break;
|
||||
case "2":
|
||||
newMode = new PercentType("55");
|
||||
break;
|
||||
case "3":
|
||||
newMode = new PercentType("60");
|
||||
break;
|
||||
case "4":
|
||||
newMode = new PercentType("100");
|
||||
break;
|
||||
}
|
||||
updateState(CHANNEL_DESIREDHUMIDITY, newMode);
|
||||
break;
|
||||
case "CurrentHumidity":
|
||||
newMode = new StringType(attributeValue);
|
||||
updateState(CHANNEL_CURRENTHUMIDITY, newMode);
|
||||
break;
|
||||
case "Temperature":
|
||||
newMode = new StringType(attributeValue);
|
||||
updateState(CHANNEL_CURRENTTEMP, newMode);
|
||||
break;
|
||||
case "SetTemperature":
|
||||
newMode = new StringType(attributeValue);
|
||||
updateState(CHANNEL_TARGETTEMP, newMode);
|
||||
break;
|
||||
case "AutoOffTime":
|
||||
newMode = new StringType(attributeValue);
|
||||
updateState(CHANNEL_AUTOOFFTIME, newMode);
|
||||
break;
|
||||
case "TimeRemaining":
|
||||
newMode = new StringType(attributeValue);
|
||||
updateState(CHANNEL_HEATINGREMAINING, newMode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException | ParserConfigurationException | SAXException | IOException e) {
|
||||
logger.debug("Failed to get actual state for device '{}':", getThing().getUID(), e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
public String getWemoURL(String actionService) {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
String wemoURL = null;
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
|
||||
return wemoURL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getCharacterDataFromElement(Element e) {
|
||||
Node child = e.getFirstChild();
|
||||
if (child instanceof CharacterData) {
|
||||
CharacterData cd = (CharacterData) child;
|
||||
return cd.getData();
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingStatusInfo;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link WemoLightHandler} is the handler for a WeMo light, responsible for handling commands and state updates for the
|
||||
* different channels of a WeMo light.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*/
|
||||
public class WemoLightHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoLightHandler.class);
|
||||
|
||||
private Map<String, Boolean> subscriptionState = new HashMap<>();
|
||||
|
||||
private UpnpIOService service;
|
||||
|
||||
private WemoBridgeHandler wemoBridgeHandler;
|
||||
|
||||
private String wemoLightID;
|
||||
|
||||
private int currentBrightness;
|
||||
|
||||
/**
|
||||
* Set dimming stepsize to 5%
|
||||
*/
|
||||
private static final int DIM_STEPSIZE = 5;
|
||||
|
||||
protected static final String SUBSCRIPTION = "bridge1";
|
||||
|
||||
protected static final int SUBSCRIPTION_DURATION = 600;
|
||||
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private final int DEFAULT_REFRESH_INTERVAL = 60;
|
||||
|
||||
/**
|
||||
* The default refresh initial delay in Seconds.
|
||||
*/
|
||||
private static final int DEFAULT_REFRESH_INITIAL_DELAY = 15;
|
||||
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Runnable refreshRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!isUpnpDeviceRegistered()) {
|
||||
logger.debug("WeMo UPnP device {} not yet registered", getUDN());
|
||||
}
|
||||
|
||||
getDeviceState();
|
||||
onSubscription();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public WemoLightHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
|
||||
super(thing);
|
||||
|
||||
this.wemoHttpCaller = wemoHttpcaller;
|
||||
|
||||
if (upnpIOService != null) {
|
||||
logger.debug("UPnPIOService '{}'", upnpIOService);
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// initialize() is only called if the required parameter 'deviceID' is available
|
||||
wemoLightID = (String) getConfig().get(DEVICE_ID);
|
||||
|
||||
if (getBridge() != null) {
|
||||
logger.debug("Initializing WemoLightHandler for LightID '{}'", wemoLightID);
|
||||
if (getBridge().getStatus() == ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
if (bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
onSubscription();
|
||||
onUpdate();
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.BRIDGE_OFFLINE);
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WeMoLightHandler disposed.");
|
||||
|
||||
removeSubscription();
|
||||
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized WemoBridgeHandler getWemoBridgeHandler() {
|
||||
if (this.wemoBridgeHandler == null) {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
logger.error("Required bridge not defined for device {}.", wemoLightID);
|
||||
return null;
|
||||
}
|
||||
ThingHandler handler = bridge.getHandler();
|
||||
if (handler instanceof WemoBridgeHandler) {
|
||||
this.wemoBridgeHandler = (WemoBridgeHandler) handler;
|
||||
} else {
|
||||
logger.debug("No available bridge handler found for {} bridge {} .", wemoLightID, bridge.getUID());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return this.wemoBridgeHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
try {
|
||||
getDeviceState();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
}
|
||||
} else {
|
||||
Configuration configuration = getConfig();
|
||||
configuration.get(DEVICE_ID);
|
||||
|
||||
WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
|
||||
if (wemoBridge == null) {
|
||||
logger.debug("wemoBridgeHandler not found, cannot handle command");
|
||||
return;
|
||||
}
|
||||
String devUDN = "uuid:" + wemoBridge.getThing().getConfiguration().get(UDN).toString();
|
||||
logger.trace("WeMo Bridge to send command to : {}", devUDN);
|
||||
|
||||
String value = null;
|
||||
String capability = null;
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_BRIGHTNESS:
|
||||
capability = "10008";
|
||||
if (command instanceof PercentType) {
|
||||
int newBrightness = ((PercentType) command).intValue();
|
||||
logger.trace("wemoLight received Value {}", newBrightness);
|
||||
int value1 = Math.round(newBrightness * 255 / 100);
|
||||
value = value1 + ":0";
|
||||
currentBrightness = newBrightness;
|
||||
} else if (command instanceof OnOffType) {
|
||||
switch (command.toString()) {
|
||||
case "ON":
|
||||
value = "255:0";
|
||||
break;
|
||||
case "OFF":
|
||||
value = "0:0";
|
||||
break;
|
||||
}
|
||||
} else if (command instanceof IncreaseDecreaseType) {
|
||||
int newBrightness;
|
||||
switch (command.toString()) {
|
||||
case "INCREASE":
|
||||
currentBrightness = currentBrightness + DIM_STEPSIZE;
|
||||
newBrightness = Math.round(currentBrightness * 255 / 100);
|
||||
if (newBrightness > 255) {
|
||||
newBrightness = 255;
|
||||
}
|
||||
value = newBrightness + ":0";
|
||||
break;
|
||||
case "DECREASE":
|
||||
currentBrightness = currentBrightness - DIM_STEPSIZE;
|
||||
newBrightness = Math.round(currentBrightness * 255 / 100);
|
||||
if (newBrightness < 0) {
|
||||
newBrightness = 0;
|
||||
}
|
||||
value = newBrightness + ":0";
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_STATE:
|
||||
capability = "10006";
|
||||
switch (command.toString()) {
|
||||
case "ON":
|
||||
value = "1";
|
||||
break;
|
||||
case "OFF":
|
||||
value = "0";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:bridge:1#SetDeviceStatus\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">"
|
||||
+ "<DeviceStatusList>"
|
||||
+ "<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><DeviceID>"
|
||||
+ wemoLightID
|
||||
+ "</DeviceID><IsGroupAction>NO</IsGroupAction><CapabilityID>"
|
||||
+ capability + "</CapabilityID><CapabilityValue>" + value
|
||||
+ "</CapabilityValue></DeviceStatus>" + "</DeviceStatusList>"
|
||||
+ "</u:SetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
String wemoURL = getWemoURL();
|
||||
|
||||
if (wemoURL != null && capability != null && value != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
if (capability.equals("10008")) {
|
||||
OnOffType binaryState = null;
|
||||
binaryState = value.equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
updateState(CHANNEL_STATE, binaryState);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Could not send command to WeMo Bridge", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
WemoBridgeHandler wemoBridge = getWemoBridgeHandler();
|
||||
if (wemoBridge == null) {
|
||||
logger.debug("wemoBridgeHandler not found");
|
||||
return null;
|
||||
}
|
||||
return (String) wemoBridge.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link getDeviceState} is used for polling the actual state of a WeMo Light and updating the according
|
||||
* channel states.
|
||||
*/
|
||||
public void getDeviceState() {
|
||||
logger.debug("Request actual state for LightID '{}'", wemoLightID);
|
||||
try {
|
||||
String soapHeader = "\"urn:Belkin:service:bridge:1#GetDeviceStatus\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:GetDeviceStatus xmlns:u=\"urn:Belkin:service:bridge:1\">" + "<DeviceIDs>"
|
||||
+ wemoLightID + "</DeviceIDs>" + "</u:GetDeviceStatus>" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
String wemoURL = getWemoURL();
|
||||
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
wemoCallResponse = StringEscapeUtils.unescapeXml(wemoCallResponse);
|
||||
String response = StringUtils.substringBetween(wemoCallResponse, "<CapabilityValue>",
|
||||
"</CapabilityValue>");
|
||||
logger.trace("wemoNewLightState = {}", response);
|
||||
String[] splitResponse = response.split(",");
|
||||
if (splitResponse[0] != null) {
|
||||
OnOffType binaryState = null;
|
||||
binaryState = splitResponse[0].equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
updateState(CHANNEL_STATE, binaryState);
|
||||
}
|
||||
if (splitResponse[1] != null) {
|
||||
String splitBrightness[] = splitResponse[1].split(":");
|
||||
if (splitBrightness[0] != null) {
|
||||
int newBrightnessValue = Integer.valueOf(splitBrightness[0]);
|
||||
int newBrightness = Math.round(newBrightnessValue * 100 / 255);
|
||||
logger.trace("newBrightness = {}", newBrightness);
|
||||
State newBrightnessState = new PercentType(newBrightness);
|
||||
updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
|
||||
currentBrightness = newBrightness;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Could not retrieve new Wemo light state", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
|
||||
new Object[] { variable, value, service, this.getThing().getUID() });
|
||||
String capabilityId = StringUtils.substringBetween(value, "<CapabilityId>", "</CapabilityId>");
|
||||
String newValue = StringUtils.substringBetween(value, "<Value>", "</Value>");
|
||||
switch (capabilityId) {
|
||||
case "10006":
|
||||
OnOffType binaryState = null;
|
||||
binaryState = newValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
updateState(CHANNEL_STATE, binaryState);
|
||||
break;
|
||||
case "10008":
|
||||
String splitValue[] = newValue.split(":");
|
||||
if (splitValue[0] != null) {
|
||||
int newBrightnessValue = Integer.valueOf(splitValue[0]);
|
||||
int newBrightness = Math.round(newBrightnessValue * 100 / 255);
|
||||
State newBrightnessState = new PercentType(newBrightness);
|
||||
updateState(CHANNEL_BRIGHTNESS, newBrightnessState);
|
||||
currentBrightness = newBrightness;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
|
||||
private synchronized void onSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Checking WeMo GENA subscription for '{}'", this);
|
||||
|
||||
if ((subscriptionState.get(SUBSCRIPTION) == null) || !subscriptionState.get(SUBSCRIPTION).booleanValue()) {
|
||||
logger.debug("Setting up GENA subscription {}: Subscribing to service {}...", getUDN(), SUBSCRIPTION);
|
||||
service.addSubscription(this, SUBSCRIPTION, SUBSCRIPTION_DURATION);
|
||||
subscriptionState.put(SUBSCRIPTION, true);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Setting up WeMo GENA subscription for '{}' FAILED - service.isRegistered(this) is FALSE",
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeSubscription() {
|
||||
if (service.isRegistered(this)) {
|
||||
logger.debug("Removing WeMo GENA subscription for '{}'", this);
|
||||
|
||||
if ((subscriptionState.get(SUBSCRIPTION) != null) && subscriptionState.get(SUBSCRIPTION).booleanValue()) {
|
||||
logger.debug("WeMo {}: Unsubscribing from service {}...", getUDN(), SUBSCRIPTION);
|
||||
service.removeSubscription(this, SUBSCRIPTION);
|
||||
}
|
||||
|
||||
subscriptionState = new HashMap<>();
|
||||
service.unregisterParticipant(this);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = DEFAULT_REFRESH_INTERVAL;
|
||||
Object refreshConfig = config.get("refresh");
|
||||
if (refreshConfig != null) {
|
||||
refreshInterval = ((BigDecimal) refreshConfig).intValue();
|
||||
}
|
||||
logger.trace("Start polling job for LightID '{}'", wemoLightID);
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, DEFAULT_REFRESH_INITIAL_DELAY,
|
||||
refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isUpnpDeviceRegistered() {
|
||||
return service.isRegistered(this);
|
||||
}
|
||||
|
||||
public String getWemoURL() {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
String wemoURL = null;
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
wemoURL = deviceURL + "/upnp/control/bridge1";
|
||||
return wemoURL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.handler;
|
||||
|
||||
import static org.openhab.binding.wemo.internal.WemoBindingConstants.*;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
import org.apache.commons.lang.StringEscapeUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.wemo.internal.http.WemoHttpCall;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOParticipant;
|
||||
import org.openhab.core.io.transport.upnp.UpnpIOService;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.CharacterData;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
/**
|
||||
* The {@link WemoMakerHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels and to update their states.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*/
|
||||
|
||||
public class WemoMakerHandler extends AbstractWemoHandler implements UpnpIOParticipant {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoMakerHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_MAKER);
|
||||
|
||||
private UpnpIOService service;
|
||||
|
||||
/**
|
||||
* The default refresh interval in Seconds.
|
||||
*/
|
||||
private final int DEFAULT_REFRESH_INTERVAL = 15;
|
||||
|
||||
private ScheduledFuture<?> refreshJob;
|
||||
|
||||
private final Runnable refreshRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
updateWemoState();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public WemoMakerHandler(Thing thing, UpnpIOService upnpIOService, WemoHttpCall wemoHttpcaller) {
|
||||
super(thing);
|
||||
|
||||
this.wemoHttpCaller = wemoHttpcaller;
|
||||
|
||||
logger.debug("Creating a WemoMakerHandler for thing '{}'", getThing().getUID());
|
||||
|
||||
if (upnpIOService != null) {
|
||||
this.service = upnpIOService;
|
||||
} else {
|
||||
logger.debug("upnpIOService not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Configuration configuration = getConfig();
|
||||
|
||||
if (configuration.get("udn") != null) {
|
||||
logger.debug("Initializing WemoMakerHandler for UDN '{}'", configuration.get("udn"));
|
||||
onUpdate();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
logger.debug("Cannot initalize WemoMakerHandler. UDN not set.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("WeMoMakerHandler disposed.");
|
||||
|
||||
if (refreshJob != null && !refreshJob.isCancelled()) {
|
||||
refreshJob.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
try {
|
||||
updateWemoState();
|
||||
} catch (Exception e) {
|
||||
logger.debug("Exception during poll", e);
|
||||
}
|
||||
} else if (channelUID.getId().equals(CHANNEL_RELAY)) {
|
||||
if (command instanceof OnOffType) {
|
||||
try {
|
||||
String binaryState = null;
|
||||
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
binaryState = "1";
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
binaryState = "0";
|
||||
}
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:basicevent:1#SetBinaryState\"";
|
||||
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">"
|
||||
+ "<BinaryState>" + binaryState + "</BinaryState>" + "</u:SetBinaryState>" + "</s:Body>"
|
||||
+ "</s:Envelope>";
|
||||
|
||||
String wemoURL = getWemoURL("basicevent");
|
||||
|
||||
if (wemoURL != null) {
|
||||
@SuppressWarnings("unused")
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send command '{}' for device '{}' ", command, getThing().getUID(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private synchronized void onSubscription() {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private synchronized void removeSubscription() {
|
||||
}
|
||||
|
||||
private synchronized void onUpdate() {
|
||||
if (service.isRegistered(this)) {
|
||||
if (refreshJob == null || refreshJob.isCancelled()) {
|
||||
Configuration config = getThing().getConfiguration();
|
||||
int refreshInterval = DEFAULT_REFRESH_INTERVAL;
|
||||
Object refreshConfig = config.get("refresh");
|
||||
if (refreshConfig != null) {
|
||||
refreshInterval = ((BigDecimal) refreshConfig).intValue();
|
||||
}
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(refreshRunnable, 0, refreshInterval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUDN() {
|
||||
return (String) this.getThing().getConfiguration().get(UDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link updateWemoState} polls the actual state of a WeMo Maker.
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
protected void updateWemoState() {
|
||||
String action = "GetAttributes";
|
||||
String actionService = "deviceevent";
|
||||
|
||||
String soapHeader = "\"urn:Belkin:service:" + actionService + ":1#" + action + "\"";
|
||||
String content = "<?xml version=\"1.0\"?>"
|
||||
+ "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
+ "<s:Body>" + "<u:" + action + " xmlns:u=\"urn:Belkin:service:" + actionService + ":1\">" + "</u:"
|
||||
+ action + ">" + "</s:Body>" + "</s:Envelope>";
|
||||
|
||||
try {
|
||||
String wemoURL = getWemoURL(actionService);
|
||||
if (wemoURL != null) {
|
||||
String wemoCallResponse = wemoHttpCaller.executeCall(wemoURL, soapHeader, content);
|
||||
if (wemoCallResponse != null) {
|
||||
try {
|
||||
String stringParser = StringUtils.substringBetween(wemoCallResponse, "<attributeList>",
|
||||
"</attributeList>");
|
||||
|
||||
// Due to Belkins bad response formatting, we need to run this twice.
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
stringParser = StringEscapeUtils.unescapeXml(stringParser);
|
||||
|
||||
logger.trace("Maker response '{}' for device '{}' received", stringParser, getThing().getUID());
|
||||
|
||||
stringParser = "<data>" + stringParser + "</data>";
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
InputSource is = new InputSource();
|
||||
is.setCharacterStream(new StringReader(stringParser));
|
||||
|
||||
Document doc = db.parse(is);
|
||||
NodeList nodes = doc.getElementsByTagName("attribute");
|
||||
|
||||
// iterate the attributes
|
||||
for (int i = 0; i < nodes.getLength(); i++) {
|
||||
Element element = (Element) nodes.item(i);
|
||||
|
||||
NodeList deviceIndex = element.getElementsByTagName("name");
|
||||
Element line = (Element) deviceIndex.item(0);
|
||||
String attributeName = getCharacterDataFromElement(line);
|
||||
logger.trace("attributeName: {}", attributeName);
|
||||
|
||||
NodeList deviceID = element.getElementsByTagName("value");
|
||||
line = (Element) deviceID.item(0);
|
||||
String attributeValue = getCharacterDataFromElement(line);
|
||||
logger.trace("attributeValue: {}", attributeValue);
|
||||
|
||||
switch (attributeName) {
|
||||
case "Switch":
|
||||
State relayState = attributeValue.equals("0") ? OnOffType.OFF : OnOffType.ON;
|
||||
if (relayState != null) {
|
||||
logger.debug("New relayState '{}' for device '{}' received", relayState,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_RELAY, relayState);
|
||||
}
|
||||
break;
|
||||
case "Sensor":
|
||||
State sensorState = attributeValue.equals("1") ? OnOffType.OFF : OnOffType.ON;
|
||||
if (sensorState != null) {
|
||||
logger.debug("New sensorState '{}' for device '{}' received", sensorState,
|
||||
getThing().getUID());
|
||||
updateState(CHANNEL_SENSOR, sensorState);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse attributeList for WeMo Maker '{}'", this.getThing().getUID(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to get attributes for device '{}'", getThing().getUID(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getWemoURL(String actionService) {
|
||||
URL descriptorURL = service.getDescriptorURL(this);
|
||||
String wemoURL = null;
|
||||
if (descriptorURL != null) {
|
||||
String deviceURL = StringUtils.substringBefore(descriptorURL.toString(), "/setup.xml");
|
||||
wemoURL = deviceURL + "/upnp/control/" + actionService + "1";
|
||||
return wemoURL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getCharacterDataFromElement(Element e) {
|
||||
Node child = e.getFirstChild();
|
||||
if (child instanceof CharacterData) {
|
||||
CharacterData cd = (CharacterData) child;
|
||||
return cd.getData();
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceSubscribed(String service, boolean succeeded) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueReceived(String variable, String value, String service) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.wemo.internal.http;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.openhab.binding.wemo.internal.WemoBindingConstants;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WemoHttpCall} is responsible for calling a WeMo device to send commands or retrieve status updates.
|
||||
*
|
||||
* @author Hans-Jörg Merk - Initial contribution
|
||||
*/
|
||||
|
||||
public class WemoHttpCall {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WemoHttpCall.class);
|
||||
|
||||
public String executeCall(String wemoURL, String soapHeader, String content) {
|
||||
try {
|
||||
Properties wemoHeaders = new Properties();
|
||||
wemoHeaders.setProperty("CONTENT-TYPE", WemoBindingConstants.HTTP_CALL_CONTENT_HEADER);
|
||||
wemoHeaders.put("SOAPACTION", soapHeader);
|
||||
|
||||
InputStream wemoContent = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String wemoCallResponse = HttpUtil.executeUrl("POST", wemoURL, wemoHeaders, wemoContent, null, 2000);
|
||||
return wemoCallResponse;
|
||||
} catch (IOException e) {
|
||||
// throw new IllegalStateException("Could not call WeMo", e);
|
||||
logger.debug("Could not make HTTP call to WeMo");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="wemo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>WeMo Binding</name>
|
||||
<description>The WeMo binding integrates Belkin WeMo devices.</description>
|
||||
<author>Hans-Jörg Merk</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="wemo"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Wemo Link -->
|
||||
<bridge-type id="bridge">
|
||||
<label>WeMo Link</label>
|
||||
<description>The Wemo Link represents the Belkin WeMo Link bridge.</description>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Belkin</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Link Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,430 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="wemo"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="socket">
|
||||
<label>WeMo Switch</label>
|
||||
<description>This is a standard WeMo Switch</description>
|
||||
|
||||
<channels>
|
||||
<channel id="state" typeId="state"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="insight">
|
||||
<label>WeMo Insight Switch</label>
|
||||
<description>This is a WeMo Insight Switch with energy measurement</description>
|
||||
|
||||
<channels>
|
||||
<channel id="state" typeId="state"/>
|
||||
<channel id="lastChangedAt" typeId="lastChangedAt"/>
|
||||
<channel id="lastOnFor" typeId="lastOnFor"/>
|
||||
<channel id="onToday" typeId="onToday"/>
|
||||
<channel id="onTotal" typeId="onTotal"/>
|
||||
<channel id="timespan" typeId="timespan"/>
|
||||
<channel id="averagePower" typeId="averagePower"/>
|
||||
<channel id="currentPower" typeId="currentPower"/>
|
||||
<channel id="energyToday" typeId="energyToday"/>
|
||||
<channel id="energyTotal" typeId="energyTotal"/>
|
||||
<channel id="standByLimit" typeId="standByLimit"/>
|
||||
<channel id="onStandBy" typeId="onStandBy"/>
|
||||
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="lightswitch">
|
||||
<label>WeMo Light Switch</label>
|
||||
<description>This is a WeMo LightSwitch</description>
|
||||
|
||||
<channels>
|
||||
<channel id="state" typeId="state"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="motion">
|
||||
<label>WeMo Motion</label>
|
||||
<description>This is a WeMo MotionSensor</description>
|
||||
|
||||
<channels>
|
||||
<channel id="motionDetection" typeId="motionDetection"/>
|
||||
<channel id="lastMotionDetected" typeId="lastMotionDetected"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Standard WeMo Bulb with E27 socket -->
|
||||
<thing-type id="MZ100">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="bridge"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>WeMo LED Light</label>
|
||||
<description>This is a standard WeMo light bulb with E27 socket </description>
|
||||
|
||||
<channels>
|
||||
<channel id="state" typeId="state"/>
|
||||
<channel id="brightness" typeId="brightness"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">MRVL</property>
|
||||
<property name="modelId">MZ100</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="deviceID" type="text">
|
||||
<label>Device ID</label>
|
||||
<description>The device ID identifies one certain WeMo light.</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="Maker">
|
||||
<label>WeMo Maker</label>
|
||||
<description>This is a WeMo Maker</description>
|
||||
|
||||
<channels>
|
||||
<channel id="relay" typeId="relay"/>
|
||||
<channel id="sensor" typeId="sensor"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="CoffeeMaker">
|
||||
<label>Mr. Coffee WeMo Enabled Coffeemaker</label>
|
||||
<description>This is a WeMo enabled coffee maker</description>
|
||||
|
||||
<channels>
|
||||
<channel id="state" typeId="state"/>
|
||||
<channel id="coffeeMode" typeId="coffeeMode"/>
|
||||
<channel id="modeTime" typeId="modeTime"/>
|
||||
<channel id="timeRemaining" typeId="timeRemaining"/>
|
||||
<channel id="waterLevelReached" typeId="waterLevelReached"/>
|
||||
<channel id="cleanAdvise" typeId="cleanAdvise"/>
|
||||
<channel id="filterAdvise" typeId="filterAdvise"/>
|
||||
<channel id="brewed" typeId="brewed"/>
|
||||
<channel id="lastCleaned" typeId="lastCleaned"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="pollingInterval" type="integer" required="false" min="15" max="180">
|
||||
<label>Polling Interval</label>
|
||||
<description>Interval polling the WeMo Coffee Maker.
|
||||
</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="dimmer">
|
||||
<label>WeMo DimmerSwitch</label>
|
||||
<description>This is a WeMo DimmerSwitch</description>
|
||||
|
||||
<channels>
|
||||
<channel id="brightness" typeId="brightness"/>
|
||||
<channel id="faderCountDownTime" typeId="faderCountDownTime"/>
|
||||
<channel id="faderEnabled" typeId="faderEnabled"/>
|
||||
<channel id="timerStart" typeId="timerStart"/>
|
||||
<channel id="nightMode" typeId="nightMode"/>
|
||||
<channel id="startTime" typeId="startTime"/>
|
||||
<channel id="endTime" typeId="endTime"/>
|
||||
<channel id="nightModeBrightness" typeId="nightModeBrightness"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="udn" type="text">
|
||||
<label>Unique Device Name</label>
|
||||
<description>The UDN identifies the WeMo Device</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="state">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Switch</label>
|
||||
<description>Turns the power on or off</description>
|
||||
<category>Switch</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="motionDetection">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Motion Status</label>
|
||||
<description>Indicates whether motion is detected or not</description>
|
||||
<category>Switch</category>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lastMotionDetected" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Activity (Date/Time)</label>
|
||||
<description>Date/time when last motion was detected</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lastChangedAt" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Activity (Date/Time)</label>
|
||||
<description>Date/time when the state last changed</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lastOnFor" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Last Activity (s)</label>
|
||||
<description>Duration the device has been switched on for</description>
|
||||
<state pattern="%.1f s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="onToday" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Today's Activity (s)</label>
|
||||
<description>How long has the device been switched on today</description>
|
||||
<state pattern="%.1f s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="onTotal" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Total Activity (s)</label>
|
||||
<description>How long has the device been switched on totally</description>
|
||||
<state pattern="%.1f s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timespan" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Usage Timespan (s)</label>
|
||||
<description>Time used to measure average usage</description>
|
||||
<state pattern="%.1f s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="averagePower" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Average Power</label>
|
||||
<description>The average power consumption</description>
|
||||
<category>Energy</category>
|
||||
<state pattern="%.1f W"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="currentPower">
|
||||
<item-type>Number</item-type>
|
||||
<label>Power</label>
|
||||
<description>The current power consumption</description>
|
||||
<category>Energy</category>
|
||||
<state pattern="%.1f W"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyToday" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Energy Today</label>
|
||||
<description>Todays power consumption</description>
|
||||
<category>Energy</category>
|
||||
<state pattern="%.1f Wh"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="energyTotal" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Energy Total</label>
|
||||
<description>Total power consumption</description>
|
||||
<category>Energy</category>
|
||||
<state pattern="%.1f Wh"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="standByLimit" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>StandBy Limit</label>
|
||||
<description>Total power consumption</description>
|
||||
<category>Energy</category>
|
||||
<state pattern="%.1f W"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="onStandBy" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>On Standby</label>
|
||||
<description>Appliance on standby</description>
|
||||
<category>Energy</category>
|
||||
<state readOnly="true"/>
|
||||
|
||||
</channel-type>
|
||||
|
||||
<!-- Brightness Channel -->
|
||||
<channel-type id="brightness">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Brightness</label>
|
||||
<description>The brightness channel allows to control the brightness of a light.
|
||||
It is also possible to switch the
|
||||
light on and off.
|
||||
</description>
|
||||
<category>DimmableLight</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="relay">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Relay</label>
|
||||
<description>Switches the integrated relay contact close/open</description>
|
||||
<category>Switch</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="sensor">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Sensor</label>
|
||||
<description>Shows the state of the integrated sensor</description>
|
||||
<category>Switch</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="coffeeMode">
|
||||
<item-type>String</item-type>
|
||||
<label>Mode</label>
|
||||
<description>Shows the operation mode of a WeMo Coffee Maker</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="Refill">Not Ready: Refill Water and Replace Carafe</option>
|
||||
<option value="PlaceCarafe">Not Ready: Replace Carafe</option>
|
||||
<option value="RefillWater">Not Ready: Refill Water</option>
|
||||
<option value="Ready">Ready</option>
|
||||
<option value="Brewing">Brewing</option>
|
||||
<option value="Brewed">Brewing Finished</option>
|
||||
<option value="CleaningBrewing">Cleaning Coffee Maker</option>
|
||||
<option value="CleaningSoaking">Cleaning Filter</option>
|
||||
<option value="BrewFailCarafeRemoved">Brewing Failed: Carafe Removed</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="modeTime">
|
||||
<item-type>Number</item-type>
|
||||
<label>ModeTime</label>
|
||||
<description>Shows the current amount of time, in minutes, that the Coffee Maker has been in the current mode</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timeRemaining">
|
||||
<item-type>Number</item-type>
|
||||
<label>TimeRemaining</label>
|
||||
<description>Shows the remaining brewing time of a WeMo Coffee Maker</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="waterLevelReached" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>WaterLevelReached</label>
|
||||
<description>Indicates if the WeMo Coffee Maker needs to be refilled</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cleanAdvise" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>CleanAdvise</label>
|
||||
<description>Indicates if a WeMo Coffee Maker needs to be cleaned</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="filterAdvise" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>FilterAdvise</label>
|
||||
<description>Indicates if a WeMo Coffee Maker needs to have the filter changed</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="brewed" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Brewed</label>
|
||||
<description>Date/time the coffee maker last completed brewing coffee</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="lastCleaned" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>LastCleaned</label>
|
||||
<description>Date/time the coffee maker last completed cleaning</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="faderCountDownTime" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Fader CountDown Time</label>
|
||||
<description>The fading duration time in minutes</description>
|
||||
<state min="1" max="30" step="1.0" pattern="%d min" readOnly="false"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="faderEnabled" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Fader OFF/ON</label>
|
||||
<description>Allows to switch the fader ON/OFF</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timerStart" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Timer OFF/ON</label>
|
||||
<description>Allows to switch the timer ON/OFF</description>
|
||||
</channel-type>
|
||||
|
||||
|
||||
<channel-type id="nightMode" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>NightMode OFF/ON</label>
|
||||
<description>Allows to switch the nightMode ON/OFF</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="startTime" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>NightMode Start Time</label>
|
||||
<description>Time when the Night Mode starts</description>
|
||||
<state pattern="%1$tR" readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="endTime" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>NightMode End Time</label>
|
||||
<description>Time when the Night Mode ends</description>
|
||||
<state pattern="%1$tR" readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="nightModeBrightness" advanced="true">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Night Mode Brightness</label>
|
||||
<description>Allows setting the brightness of Night Mode</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user